diff --git a/src/controllers/folder.controller.ts b/src/controllers/folder.controller.ts index e69de29..a2c5c5a 100644 --- a/src/controllers/folder.controller.ts +++ b/src/controllers/folder.controller.ts @@ -0,0 +1,141 @@ +import { FolderModel, type folderType } from "../models/folder.model"; +import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; +import { checkDuplicateItemName } from "../utils/checkDuplicates"; + +export const createFolder = async (foldersFields: folderType) => { + try { + if(await checkDuplicateItemName(foldersFields.itemName)){ + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Duplicate folder name", + ) + } + + const newFolders = new FolderModel(foldersFields); + await newFolders.save(); + return newFolders; + } catch (err: unknown) { + if (err instanceof HttpError) { + throw err; + } + + checkMongooseErrors(err); + + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "Folder creation failed", + { cause: err }, + ); + } +}; + +export const getAllFolders = async (user: string) => { + try { + const folders = await FolderModel.find({ user: user }); + return folders; + } catch (err: unknown) { + //rethrow any errors as HttpErrors + if (err instanceof HttpError) { + throw err; + } + //checks if mongoose threw and will rethrow with appropriate status code and message + checkMongooseErrors(err); + + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "Folders retrieval failed", + { cause: err }, + ); + } +} + +export const getFolderById = async (user: string, folderId: string) => { + try { + const folder = await FolderModel.findOne({ + user: user, + _id: folderId, + }); + return folder; + } catch (err: unknown) { + //rethrow any errors as HttpErrors + if (err instanceof HttpError) { + throw err; + } + //checks if mongoose threw and will rethrow with appropriate status code and message + checkMongooseErrors(err); + + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "Folder retrieval failed", + { cause: err }, + ); + } +}; + +export const updateFolder = async ( + user: string, + folderId: string, + foldersFields: folderType, +) => { + try { + if (!folderId) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Missing folder ID for update", + ); + } + + if (await checkDuplicateItemName(foldersFields.itemName, folderId)) { + throw new HttpError(HttpStatus.BAD_REQUEST, "Duplicate item name"); + } + + const updatedFolder = await FolderModel.findOneAndUpdate( + { _id: folderId, user: user }, // Query to match the document by _id and user + { $set: foldersFields }, // Update operation + { new: true, runValidators: true }, // Options: return the updated document and run schema validators + ); + return updatedFolder; + } catch (err: unknown) { + //rethrow any errors as HttpErrors + if (err instanceof HttpError) { + throw err; + } + //checks if mongoose threw and will rethrow with appropriate status code and message + checkMongooseErrors(err); + + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "Folder update failed", + { cause: err }, + ); + } +}; + +export const deleteFolder = async (user: string, folderId: string) => { + try { + const deletedFolder = await FolderModel.findOneAndDelete({ + _id: folderId, + user: user, + }); + if (!deletedFolder) { + throw new HttpError( + HttpStatus.NOT_FOUND, + "Folder not found or already deleted", + ); + } + return { message: "Folder deleted successfully" }; + } catch (err: unknown) { + //rethrow any errors as HttpErrors + if (err instanceof HttpError) { + throw err; + } + //checks if mongoose threw and will rethrow with appropriate status code and message + checkMongooseErrors(err); + + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "Folder deletion failed", + { cause: err }, + ); + } +}; \ No newline at end of file diff --git a/src/controllers/resume.controller.ts b/src/controllers/resume.controller.ts index 6ee7f04..47125c5 100644 --- a/src/controllers/resume.controller.ts +++ b/src/controllers/resume.controller.ts @@ -1,6 +1,8 @@ import { ResumeModel, type resumeType } from "../models/resume.model"; import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; import { checkDuplicateItemName } from "../utils/checkDuplicates"; +import { FolderModel } from "../models/folder.model"; +import mongoose from "mongoose"; export const createResume = async (resumesFields: resumeType) => { try { @@ -113,6 +115,10 @@ export const updateResume = async ( export const deleteResume = async (user: string, resumeId: string) => { try { + await FolderModel.updateMany( + { resumeIds: new mongoose.Types.ObjectId(resumeId) }, + { $pull: { resumeIds: new mongoose.Types.ObjectId(resumeId) } } + ); const deletedResume = await ResumeModel.findOneAndDelete({ _id: resumeId, user: user, diff --git a/src/models/folder.model.ts b/src/models/folder.model.ts index e9414ce..aecc8ae 100644 --- a/src/models/folder.model.ts +++ b/src/models/folder.model.ts @@ -6,14 +6,14 @@ const Schema = mongoose.Schema; //typescript type corresponding with the mongoose schema structure export interface folderType extends mongoose.Document { user: string; - name: string; - resumeIds: mongoose.Schema.Types.ObjectId[]; - folderIds: mongoose.Schema.Types.ObjectId[]; + itemName: string; + resumeIds: mongoose.Types.ObjectId[]; + folderIds: mongoose.Types.ObjectId[]; } const Folder = new Schema({ user: { type: String, required: true }, - name: { type: String, required: true }, + itemName: { type: String, required: true }, resumeIds: { type: [Schema.Types.ObjectId], required: true, ref: 'ResumeModel' }, folderIds: { type: [Schema.Types.ObjectId], required: true, ref: 'FolderModel' }, }); diff --git a/src/routers/folder.router.ts b/src/routers/folder.router.ts index e69de29..dcef9b9 100644 --- a/src/routers/folder.router.ts +++ b/src/routers/folder.router.ts @@ -0,0 +1,118 @@ +import { Router, type Request, type Response } from "express"; +import { + createFolder, + getAllFolders, + getFolderById, + updateFolder, + deleteFolder, +} from "../controllers/folder.controller"; +import { HttpError, HttpStatus } from "../utils/errors"; +import { type folderType } from "../models/folder.model"; + +export const folderRouter = Router(); + +//Add an folder +//Note that the user field (which is part of foldersType) in body is automatically populated by verifyToken middleware +folderRouter.post( + "/", + async (req: Request, res: Response) => { + try { + const folder = await createFolder(req.body); + res.status(HttpStatus.OK).json(folder); + } catch (err: unknown) { + if (err instanceof HttpError) { + res.status(err.errorCode).json({ error: err.message }); + } else { + res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ error: "An unknown error occurred" }); + } + } + }, +); + +//Get all folders +folderRouter.get( + "/", + async (req: Request, res: Response) => { + try { + const folder = await getAllFolders(req.body.user); + res.status(HttpStatus.OK).json(folder); + } catch (err: unknown) { + if (err instanceof HttpError) { + res.status(err.errorCode).json({ error: err.message }); + } else { + res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ error: "An unknown error occurred" }); + } + } + }, +); + +//Get a single folder by id +folderRouter.get( + "/:folderId", + async (req: Request, res: Response) => { + try { + const folder = await getFolderById( + req.body.user, + req.params.folderId, + ); + res.status(HttpStatus.OK).json(folder); + } catch (err: unknown) { + if (err instanceof HttpError) { + res.status(err.errorCode).json({ error: err.message }); + } else { + res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ error: "An unknown error occurred" }); + } + } + }, +); + +//Update an folder +folderRouter.put( + "/:folderId", + async (req: Request, res: Response) => { + try { + const folder = await updateFolder( + req.body.user, + req.params.folderId, + req.body, + ); + res.status(HttpStatus.OK).json(folder); + } catch (err: unknown) { + if (err instanceof HttpError) { + res.status(err.errorCode).json({ error: err.message }); + } else { + res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ error: "An unknown error occurred" }); + } + } + }, +); + +//Delete an folder +folderRouter.delete( + "/:folderId", + async (req: Request, res: Response) => { + try { + const folder = await deleteFolder( + req.body.user, + req.params.folderId, + ); + res.status(HttpStatus.OK).json(folder); + } catch (err: unknown) { + if (err instanceof HttpError) { + res.status(err.errorCode).json({ error: err.message }); + } else { + res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ error: "An unknown error occurred" }); + } + } + }, +); diff --git a/src/tests/controllers.tests/dummyData.ts b/src/tests/controllers.tests/dummyData.ts index 58d8272..48ac6a7 100644 --- a/src/tests/controllers.tests/dummyData.ts +++ b/src/tests/controllers.tests/dummyData.ts @@ -69,3 +69,10 @@ export const sectionHeadingDummyData1 = { itemName: "sectionHeadingItem1", title: "test section heading", } + +export const folderDummyData1 = { + user: "test", + itemName: "FolderItem1", + resumeIds: [new mongoose.Types.ObjectId("65e4f54db1e12e776e01cf31")], + folderIds: [new mongoose.Types.ObjectId("75e4f54db1e12e776e01cf31")], +} diff --git a/src/tests/controllers.tests/folder.test.ts b/src/tests/controllers.tests/folder.test.ts new file mode 100644 index 0000000..5daaaba --- /dev/null +++ b/src/tests/controllers.tests/folder.test.ts @@ -0,0 +1,89 @@ +import { dbConnect, dbDisconnect } from "../dbHandler"; +import { type folderType } from "../../models/folder.model"; +import { type resumeType } from "../../models/resume.model"; +import { folderDummyData1, activityDummyData1, resumeDummyData1 } from "./dummyData"; +import { + createFolder, + getAllFolders, + getFolderById, + updateFolder, + deleteFolder, +} from "../../controllers/folder.controller"; +import { createResume, deleteResume } from "../../controllers/resume.controller"; +import { describe, test, expect, beforeEach, afterEach } from "vitest"; +import mongoose from "mongoose"; + + +describe("Folder controller tests", () => { + beforeEach(async () => dbConnect()); + afterEach(async () => dbDisconnect()); + + test("Adds and retrieves a folders", async () => { + await createFolder(folderDummyData1 as folderType); + const returnedFolders = await getAllFolders(folderDummyData1.user); + + //get back the 1 folders that was added + expect(returnedFolders.length).to.equal(1); + expect(returnedFolders[0]).toMatchObject(folderDummyData1); + + //Can't add duplicate name + await expect( + createFolder(folderDummyData1 as folderType), + ).rejects.toThrowError(); + + const returnedFolders2 = await getAllFolders(folderDummyData1.user); + + //if duplicate, shouldn't add to db + expect(returnedFolders2.length).to.equal(1); + + const returnedFolders3 = await getAllFolders("fakeuserid"); + + //don't get records for a different user id + expect(returnedFolders3.length).to.equal(0); + }); + + test("Finds, updates, and deletes a folder", async () => { + await createFolder(folderDummyData1 as folderType); + const returnedRe = await getAllFolders(folderDummyData1.user); + + const returnedFolders = await getFolderById( + folderDummyData1.user, + returnedRe[0]._id, + ); + + expect(returnedFolders).toMatchObject(folderDummyData1); + + const newItemName = "foldersItem2"; + await updateFolder(folderDummyData1.user, returnedRe[0]._id, { + ...folderDummyData1, + itemName: newItemName, + } as folderType); + const returnedFolders2 = await getFolderById( + folderDummyData1.user, + returnedRe[0]._id, + ); + expect(returnedFolders2?.itemName).to.equal(newItemName); + + await deleteFolder(folderDummyData1.user, returnedRe[0]._id); + const returnedFolders3 = await getAllFolders(folderDummyData1.user); + expect(returnedFolders3.length).to.equal(0); + + await expect( + updateFolder(folderDummyData1.user, "", {} as folderType), + ).rejects.toThrowError("Missing"); + }); + + test("Correctly updates folder array upon item deletion", async () => { + const newResume = await createResume(resumeDummyData1 as resumeType); + let folderDummyData2 = structuredClone(folderDummyData1); + folderDummyData2.resumeIds = [newResume._id]; + folderDummyData2.folderIds = [new mongoose.Types.ObjectId("75e4f54db1e12e776e01cf31")]; + + + const origFolder = await createFolder(folderDummyData2 as folderType); + await deleteResume(resumeDummyData1.user, newResume._id); + const updatedFolder = await getFolderById(origFolder.user, origFolder._id); + + expect(updatedFolder?.resumeIds).toHaveLength(0); + }) +}); diff --git a/src/utils/checkDuplicates.ts b/src/utils/checkDuplicates.ts index 57085b6..67c388c 100644 --- a/src/utils/checkDuplicates.ts +++ b/src/utils/checkDuplicates.ts @@ -6,6 +6,8 @@ import { HeadingModel } from "../models/heading.model"; import { ProjectModel } from "../models/project.model"; import { SectionHeadingModel } from "../models/sectionHeading.model"; import { SkillsModel } from "../models/skills.model"; +import { FolderModel } from "../models/folder.model"; +import { ResumeModel } from "../models/resume.model"; export const checkDuplicateItemName = async (value: string, excludedId: string | null = null): Promise => { const field = "itemName"; @@ -17,6 +19,8 @@ export const checkDuplicateItemName = async (value: string, excludedId: string | ProjectModel, SectionHeadingModel, SkillsModel, + FolderModel, + ResumeModel, ]; // Check each model for the count of documents with the specified itemName value