From b6f2bdab793d1f86da427272979220cc0a1c5006 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Wed, 14 Feb 2024 19:52:07 -0600 Subject: [PATCH 01/37] resume model blank --- src/models/resume.model.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/models/resume.model.ts diff --git a/src/models/resume.model.ts b/src/models/resume.model.ts new file mode 100644 index 0000000..e69de29 From 4c073acf92967263c1bb0518a705729921e8caf5 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Wed, 14 Feb 2024 20:15:42 -0600 Subject: [PATCH 02/37] adds resume model --- src/models/resume.model.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/models/resume.model.ts b/src/models/resume.model.ts index e69de29..252594a 100644 --- a/src/models/resume.model.ts +++ b/src/models/resume.model.ts @@ -0,0 +1,15 @@ +//mongoose object for defining structure of all the documents in a mongodb collection +import mongoose, { Mongoose } from "mongoose"; + +const Schema = mongoose.Schema; + +//typescript type corresponding with the mongoose schema structure +export interface resumeType extends mongoose.Document{ + itemIds: mongoose.Schema.Types.ObjectId[], + templateId: mongoose.Schema.Types.ObjectId, +} + +const Event = new Schema({ + itemIds: {type: [Schema.Types.ObjectId], required: true}, + templateId: {type: Schema.Types.ObjectId, required: true} +}) \ No newline at end of file From 51290a793a4b331cf4def667569fd7e29593b723 Mon Sep 17 00:00:00 2001 From: seancrich11 Date: Wed, 14 Feb 2024 20:17:51 -0600 Subject: [PATCH 03/37] Added models --- src/models/activities.model.ts | 27 +++++++++++++++++++++++++++ src/models/education.model.ts | 23 +++++++++++++++++++++++ src/models/experience.model.ts | 26 ++++++++++++++++++++++++++ src/models/heading.model.ts | 24 ++++++++++++++++++++++++ src/models/project.model.ts | 19 +++++++++++++++++++ src/models/skills.model.ts | 20 ++++++++++++++++++++ 6 files changed, 139 insertions(+) create mode 100644 src/models/activities.model.ts create mode 100644 src/models/education.model.ts create mode 100644 src/models/experience.model.ts create mode 100644 src/models/heading.model.ts create mode 100644 src/models/project.model.ts create mode 100644 src/models/skills.model.ts diff --git a/src/models/activities.model.ts b/src/models/activities.model.ts new file mode 100644 index 0000000..1dbe66a --- /dev/null +++ b/src/models/activities.model.ts @@ -0,0 +1,27 @@ +import mongoose from "mongoose"; + +const Schema = mongoose.Schema; + +// Interface for Activities document +export interface ActivitiesType extends mongoose.Document { + id: string; + s3FileId: string; + description: string; + title: string; + subtitle: string; + year: string; + location: string; +} + +// Activities Schema +const Activities = new Schema({ + id: { type: String, required: true }, + s3FileId: { type: String, required: true }, + description: { type: String, required: true }, + title: { type: String, required: true }, + subtitle: { type: String, required: true }, + year: { type: String, required: true }, + location: { type: String, required: true } +}); + +export { Activities }; diff --git a/src/models/education.model.ts b/src/models/education.model.ts new file mode 100644 index 0000000..c6afcc0 --- /dev/null +++ b/src/models/education.model.ts @@ -0,0 +1,23 @@ +import mongoose from "mongoose"; + +const Schema = mongoose.Schema; + +// Interface for Education document +export interface EducationType extends mongoose.Document { + description: string; + title: string; + subtitle: string; + location: string; + year: string; +} + +// Education Schema +const Education = new Schema({ + description: { type: String, required: true }, + title: { type: String, required: true }, + subtitle: { type: String, required: true }, + location: { type: String, required: true }, + year: { type: String, required: true } +}); + +export { Education }; \ No newline at end of file diff --git a/src/models/experience.model.ts b/src/models/experience.model.ts new file mode 100644 index 0000000..20be841 --- /dev/null +++ b/src/models/experience.model.ts @@ -0,0 +1,26 @@ + +//get default import from mongoose library (which is used for interacting w/ mongodb) +import mongoose from "mongoose"; + +//mongoose object for defining structure of all the documents in a mongodb collection +const Schema = mongoose.Schema; + +//typescript type corresponding with the mongoose schema structure +export interface experienceType extends mongoose.Document{ + description: string; + title: string; + subtitle: string; + date: string; + location: string; +} + +//mongoose schema for an event document +const Experience = new Schema({ + description: { type: String, required: true }, + title: { type: String, required: true }, + subtitle: { type: String, required: true }, + date: { type: String, required: true }, + location: { type: String, required: true } + }); + + export { Experience }; \ No newline at end of file diff --git a/src/models/heading.model.ts b/src/models/heading.model.ts new file mode 100644 index 0000000..83f7ce3 --- /dev/null +++ b/src/models/heading.model.ts @@ -0,0 +1,24 @@ +import mongoose from "mongoose"; + +const Schema = mongoose.Schema; + +// Interface for Contact details document +export interface HeadingType extends mongoose.Document { + name: string; + id: string; + phoneNumber: string; + email: string; + linkedIn: string; + websitesGitHub: string; +} + +// Contact Schema +const Heading = new Schema({ + name: { type: String, required: true }, + phoneNumber: { type: String, required: true }, + email: { type: String, required: true }, + linkedIn: { type: String, required: true }, + websitesGitHub: { type: String, required: true } +}); + +export { Heading }; \ No newline at end of file diff --git a/src/models/project.model.ts b/src/models/project.model.ts new file mode 100644 index 0000000..36a1019 --- /dev/null +++ b/src/models/project.model.ts @@ -0,0 +1,19 @@ +import mongoose from "mongoose"; + +const Schema = mongoose.Schema; + +// Interface for Project document +export interface ProjectType extends mongoose.Document { + description: string; + title: string; + year: string; +} + +// Project Schema +const Project = new Schema({ + description: { type: String, required: true }, + title: { type: String, required: true }, + year: { type: String, required: true } +}); + +export { Project }; \ No newline at end of file diff --git a/src/models/skills.model.ts b/src/models/skills.model.ts new file mode 100644 index 0000000..cb5fa49 --- /dev/null +++ b/src/models/skills.model.ts @@ -0,0 +1,20 @@ +import mongoose from "mongoose"; + +const Schema = mongoose.Schema; + + +// Interface for Skills document +export interface SkillsType extends mongoose.Document { + title: string; + description: string; + id: string; + s3FileId: string; +} + +// Skills Schema +const Skills = new Schema({ + title: { type: String, required: true }, + description: { type: String, required: true }, +}); + +export { Skills }; From 0a807682f2d6f828f2b3011a3fc9610bdf446221 Mon Sep 17 00:00:00 2001 From: seancrich11 Date: Wed, 14 Feb 2024 20:20:55 -0600 Subject: [PATCH 04/37] Added models with formatting --- src/models/activities.model.ts | 28 ++++++++++++++-------------- src/models/education.model.ts | 22 +++++++++++----------- src/models/experience.model.ts | 27 +++++++++++++-------------- src/models/heading.model.ts | 24 ++++++++++++------------ src/models/project.model.ts | 14 +++++++------- src/models/skills.model.ts | 13 ++++++------- 6 files changed, 63 insertions(+), 65 deletions(-) diff --git a/src/models/activities.model.ts b/src/models/activities.model.ts index 1dbe66a..f0f3435 100644 --- a/src/models/activities.model.ts +++ b/src/models/activities.model.ts @@ -4,24 +4,24 @@ const Schema = mongoose.Schema; // Interface for Activities document export interface ActivitiesType extends mongoose.Document { - id: string; - s3FileId: string; - description: string; - title: string; - subtitle: string; - year: string; - location: string; + id: string; + s3FileId: string; + description: string; + title: string; + subtitle: string; + year: string; + location: string; } // Activities Schema const Activities = new Schema({ - id: { type: String, required: true }, - s3FileId: { type: String, required: true }, - description: { type: String, required: true }, - title: { type: String, required: true }, - subtitle: { type: String, required: true }, - year: { type: String, required: true }, - location: { type: String, required: true } + id: { type: String, required: true }, + s3FileId: { type: String, required: true }, + description: { type: String, required: true }, + title: { type: String, required: true }, + subtitle: { type: String, required: true }, + year: { type: String, required: true }, + location: { type: String, required: true }, }); export { Activities }; diff --git a/src/models/education.model.ts b/src/models/education.model.ts index c6afcc0..56cd80c 100644 --- a/src/models/education.model.ts +++ b/src/models/education.model.ts @@ -4,20 +4,20 @@ const Schema = mongoose.Schema; // Interface for Education document export interface EducationType extends mongoose.Document { - description: string; - title: string; - subtitle: string; - location: string; - year: string; + description: string; + title: string; + subtitle: string; + location: string; + year: string; } // Education Schema const Education = new Schema({ - description: { type: String, required: true }, - title: { type: String, required: true }, - subtitle: { type: String, required: true }, - location: { type: String, required: true }, - year: { type: String, required: true } + description: { type: String, required: true }, + title: { type: String, required: true }, + subtitle: { type: String, required: true }, + location: { type: String, required: true }, + year: { type: String, required: true }, }); -export { Education }; \ No newline at end of file +export { Education }; diff --git a/src/models/experience.model.ts b/src/models/experience.model.ts index 20be841..6a8a5ae 100644 --- a/src/models/experience.model.ts +++ b/src/models/experience.model.ts @@ -1,4 +1,3 @@ - //get default import from mongoose library (which is used for interacting w/ mongodb) import mongoose from "mongoose"; @@ -6,21 +5,21 @@ import mongoose from "mongoose"; const Schema = mongoose.Schema; //typescript type corresponding with the mongoose schema structure -export interface experienceType extends mongoose.Document{ - description: string; - title: string; - subtitle: string; - date: string; - location: string; +export interface experienceType extends mongoose.Document { + description: string; + title: string; + subtitle: string; + date: string; + location: string; } //mongoose schema for an event document const Experience = new Schema({ - description: { type: String, required: true }, - title: { type: String, required: true }, - subtitle: { type: String, required: true }, - date: { type: String, required: true }, - location: { type: String, required: true } - }); + description: { type: String, required: true }, + title: { type: String, required: true }, + subtitle: { type: String, required: true }, + date: { type: String, required: true }, + location: { type: String, required: true }, +}); - export { Experience }; \ No newline at end of file +export { Experience }; diff --git a/src/models/heading.model.ts b/src/models/heading.model.ts index 83f7ce3..7957884 100644 --- a/src/models/heading.model.ts +++ b/src/models/heading.model.ts @@ -4,21 +4,21 @@ const Schema = mongoose.Schema; // Interface for Contact details document export interface HeadingType extends mongoose.Document { - name: string; - id: string; - phoneNumber: string; - email: string; - linkedIn: string; - websitesGitHub: string; + name: string; + id: string; + phoneNumber: string; + email: string; + linkedIn: string; + websitesGitHub: string; } // Contact Schema const Heading = new Schema({ - name: { type: String, required: true }, - phoneNumber: { type: String, required: true }, - email: { type: String, required: true }, - linkedIn: { type: String, required: true }, - websitesGitHub: { type: String, required: true } + name: { type: String, required: true }, + phoneNumber: { type: String, required: true }, + email: { type: String, required: true }, + linkedIn: { type: String, required: true }, + websitesGitHub: { type: String, required: true }, }); -export { Heading }; \ No newline at end of file +export { Heading }; diff --git a/src/models/project.model.ts b/src/models/project.model.ts index 36a1019..f170031 100644 --- a/src/models/project.model.ts +++ b/src/models/project.model.ts @@ -4,16 +4,16 @@ const Schema = mongoose.Schema; // Interface for Project document export interface ProjectType extends mongoose.Document { - description: string; - title: string; - year: string; + description: string; + title: string; + year: string; } // Project Schema const Project = new Schema({ - description: { type: String, required: true }, - title: { type: String, required: true }, - year: { type: String, required: true } + description: { type: String, required: true }, + title: { type: String, required: true }, + year: { type: String, required: true }, }); -export { Project }; \ No newline at end of file +export { Project }; diff --git a/src/models/skills.model.ts b/src/models/skills.model.ts index cb5fa49..01a1140 100644 --- a/src/models/skills.model.ts +++ b/src/models/skills.model.ts @@ -2,19 +2,18 @@ import mongoose from "mongoose"; const Schema = mongoose.Schema; - // Interface for Skills document export interface SkillsType extends mongoose.Document { - title: string; - description: string; - id: string; - s3FileId: string; + title: string; + description: string; + id: string; + s3FileId: string; } // Skills Schema const Skills = new Schema({ - title: { type: String, required: true }, - description: { type: String, required: true }, + title: { type: String, required: true }, + description: { type: String, required: true }, }); export { Skills }; From 5987f3071495a6b912476b6352db5cb4b26b82d9 Mon Sep 17 00:00:00 2001 From: seancrich11 Date: Wed, 14 Feb 2024 20:24:31 -0600 Subject: [PATCH 05/37] Added Model variables --- src/models/activities.model.ts | 2 +- src/models/education.model.ts | 2 +- src/models/experience.model.ts | 2 +- src/models/heading.model.ts | 2 +- src/models/project.model.ts | 2 +- src/models/skills.model.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/models/activities.model.ts b/src/models/activities.model.ts index f0f3435..fbef005 100644 --- a/src/models/activities.model.ts +++ b/src/models/activities.model.ts @@ -24,4 +24,4 @@ const Activities = new Schema({ location: { type: String, required: true }, }); -export { Activities }; +export const ActivitiesModel = mongoose.model("Activities", Activities); \ No newline at end of file diff --git a/src/models/education.model.ts b/src/models/education.model.ts index 56cd80c..a2ff6f2 100644 --- a/src/models/education.model.ts +++ b/src/models/education.model.ts @@ -20,4 +20,4 @@ const Education = new Schema({ year: { type: String, required: true }, }); -export { Education }; +export const EducationModel = mongoose.model("Education", Education); diff --git a/src/models/experience.model.ts b/src/models/experience.model.ts index 6a8a5ae..a1e1aa2 100644 --- a/src/models/experience.model.ts +++ b/src/models/experience.model.ts @@ -22,4 +22,4 @@ const Experience = new Schema({ location: { type: String, required: true }, }); -export { Experience }; +export const ExperienceModel = mongoose.model("Experience", Experience); diff --git a/src/models/heading.model.ts b/src/models/heading.model.ts index 7957884..c0c0aa3 100644 --- a/src/models/heading.model.ts +++ b/src/models/heading.model.ts @@ -21,4 +21,4 @@ const Heading = new Schema({ websitesGitHub: { type: String, required: true }, }); -export { Heading }; +export const HeadingModel = mongoose.model("Heading", Heading); diff --git a/src/models/project.model.ts b/src/models/project.model.ts index f170031..17fbcb8 100644 --- a/src/models/project.model.ts +++ b/src/models/project.model.ts @@ -16,4 +16,4 @@ const Project = new Schema({ year: { type: String, required: true }, }); -export { Project }; +export const ProjectModel = mongoose.model("Project", Project); diff --git a/src/models/skills.model.ts b/src/models/skills.model.ts index 01a1140..9b4db90 100644 --- a/src/models/skills.model.ts +++ b/src/models/skills.model.ts @@ -16,4 +16,4 @@ const Skills = new Schema({ description: { type: String, required: true }, }); -export { Skills }; +export const SkillsModel = mongoose.model("Skills", Skills); From 1da7113927bc3c83e83784c5466a2d53c0bc87f0 Mon Sep 17 00:00:00 2001 From: seancrich11 Date: Wed, 14 Feb 2024 20:25:42 -0600 Subject: [PATCH 06/37] got rid of IDs --- src/models/activities.model.ts | 4 ---- src/models/heading.model.ts | 1 - src/models/skills.model.ts | 2 -- 3 files changed, 7 deletions(-) diff --git a/src/models/activities.model.ts b/src/models/activities.model.ts index fbef005..451206e 100644 --- a/src/models/activities.model.ts +++ b/src/models/activities.model.ts @@ -4,8 +4,6 @@ const Schema = mongoose.Schema; // Interface for Activities document export interface ActivitiesType extends mongoose.Document { - id: string; - s3FileId: string; description: string; title: string; subtitle: string; @@ -15,8 +13,6 @@ export interface ActivitiesType extends mongoose.Document { // Activities Schema const Activities = new Schema({ - id: { type: String, required: true }, - s3FileId: { type: String, required: true }, description: { type: String, required: true }, title: { type: String, required: true }, subtitle: { type: String, required: true }, diff --git a/src/models/heading.model.ts b/src/models/heading.model.ts index c0c0aa3..f7c2f96 100644 --- a/src/models/heading.model.ts +++ b/src/models/heading.model.ts @@ -5,7 +5,6 @@ const Schema = mongoose.Schema; // Interface for Contact details document export interface HeadingType extends mongoose.Document { name: string; - id: string; phoneNumber: string; email: string; linkedIn: string; diff --git a/src/models/skills.model.ts b/src/models/skills.model.ts index 9b4db90..8c46b31 100644 --- a/src/models/skills.model.ts +++ b/src/models/skills.model.ts @@ -6,8 +6,6 @@ const Schema = mongoose.Schema; export interface SkillsType extends mongoose.Document { title: string; description: string; - id: string; - s3FileId: string; } // Skills Schema From acbb30cc591391904f4bc2e8416ad9e4522dd865 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Wed, 14 Feb 2024 20:25:52 -0600 Subject: [PATCH 07/37] adds folder model --- src/models/folder.model.ts | 19 +++++++++++++++++++ src/models/resume.model.ts | 18 ++++++++++-------- 2 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 src/models/folder.model.ts diff --git a/src/models/folder.model.ts b/src/models/folder.model.ts new file mode 100644 index 0000000..68b2b94 --- /dev/null +++ b/src/models/folder.model.ts @@ -0,0 +1,19 @@ +//mongoose object for defining structure of all the documents in a mongodb collection +import mongoose from "mongoose"; + +const Schema = mongoose.Schema; + +//typescript type corresponding with the mongoose schema structure +export interface folderType extends mongoose.Document { + name: string; + resumeIds: mongoose.Schema.Types.ObjectId[]; + folderIds: mongoose.Schema.Types.ObjectId[]; +} + +const Folder = new Schema({ + name: { type: String, required: true }, + resumeIds: { type: [Schema.Types.ObjectId], required: true, ref: 'ResumeModel' }, + folderIds: { type: [Schema.Types.ObjectId], required: true, ref: 'FolderModel' }, +}); + +export const FolderModel = mongoose.model("Folder", Folder); \ No newline at end of file diff --git a/src/models/resume.model.ts b/src/models/resume.model.ts index 252594a..c8b63a7 100644 --- a/src/models/resume.model.ts +++ b/src/models/resume.model.ts @@ -1,15 +1,17 @@ //mongoose object for defining structure of all the documents in a mongodb collection -import mongoose, { Mongoose } from "mongoose"; +import mongoose from "mongoose"; const Schema = mongoose.Schema; //typescript type corresponding with the mongoose schema structure -export interface resumeType extends mongoose.Document{ - itemIds: mongoose.Schema.Types.ObjectId[], - templateId: mongoose.Schema.Types.ObjectId, +export interface resumeType extends mongoose.Document { + itemIds: mongoose.Schema.Types.ObjectId[]; + templateId: mongoose.Schema.Types.ObjectId; } -const Event = new Schema({ - itemIds: {type: [Schema.Types.ObjectId], required: true}, - templateId: {type: Schema.Types.ObjectId, required: true} -}) \ No newline at end of file +const Resume = new Schema({ + itemIds: { type: [Schema.Types.ObjectId], required: true }, + templateId: { type: Schema.Types.ObjectId, required: true }, +}); + +export const ResumeModel = mongoose.model("Resume", Resume); \ No newline at end of file From 2b023137a431dddbfb2bf20f4f5d115eca43ce32 Mon Sep 17 00:00:00 2001 From: seancrich11 Date: Wed, 14 Feb 2024 21:04:27 -0600 Subject: [PATCH 08/37] skills controller and router --- src/controllers/skills.controller.ts | 22 ++++++++++++++++++++++ src/index.ts | 2 ++ src/routers/root.router.ts | 6 ++++++ src/routers/skills.router.ts | 24 ++++++++++++++++++++++++ 4 files changed, 54 insertions(+) create mode 100644 src/controllers/skills.controller.ts create mode 100644 src/routers/root.router.ts create mode 100644 src/routers/skills.router.ts diff --git a/src/controllers/skills.controller.ts b/src/controllers/skills.controller.ts new file mode 100644 index 0000000..a2f3fce --- /dev/null +++ b/src/controllers/skills.controller.ts @@ -0,0 +1,22 @@ +import { SkillsModel, type SkillsType } from "../models/skills.model"; +import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; + +export const createSkills = async (skillsFields: SkillsType) => { + try { + const newSkills = new SkillsModel(skillsFields); + await newSkills.save(); + return newSkills; + } catch (err: unknown) { + if (err instanceof HttpError) { + throw err; + } + + checkMongooseErrors(err); + + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "Event creation failed", + { cause: err }, + ); + } +}; diff --git a/src/index.ts b/src/index.ts index 65cb442..0a5b9d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import cors from "cors"; import morgan from "morgan"; import helmet from "helmet"; import { exampleRoute } from "./routers/exampleRoute"; +import { router } from "./routers/root.router"; import { verifyToken } from "./middlewares/verifyToken"; import { notFound, errorHandler } from "./middlewares/errors"; import { connectDb } from "./utils/mongodb"; @@ -24,6 +25,7 @@ app.use(helmet()); * Uses the verifyToken middleware to protect the "/data" route * Use the verifyToken to protect all the routes that require authentication */ +app.use("/api", verifyToken, router); app.use("/example", verifyToken, exampleRoute); // Default route: Unprotected diff --git a/src/routers/root.router.ts b/src/routers/root.router.ts new file mode 100644 index 0000000..cdef0f7 --- /dev/null +++ b/src/routers/root.router.ts @@ -0,0 +1,6 @@ +import { Router } from "express"; +import { skillsRouter } from "./skills.router"; + +export const router = Router(); + +router.use("/skills", skillsRouter); diff --git a/src/routers/skills.router.ts b/src/routers/skills.router.ts new file mode 100644 index 0000000..d696b26 --- /dev/null +++ b/src/routers/skills.router.ts @@ -0,0 +1,24 @@ +import { Router, type Request, type Response } from "express"; +import { createSkills } from "../controllers/skills.controller"; +import { HttpError, HttpStatus } from "../utils/errors"; +import { type SkillsType } from "../models/skills.model"; + +export const skillsRouter = Router(); + +skillsRouter.post( + "/", + async (req: Request, res: Response) => { + try { + const skills = await createSkills(req.body); + res.status(HttpStatus.OK).json(event); + } 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" }); + } + } + }, +); From 10c699fbf2309fc79eb9da4ea32005cfefe995a8 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Thu, 15 Feb 2024 08:53:45 -0600 Subject: [PATCH 09/37] fixes skills router --- src/routers/skills.router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routers/skills.router.ts b/src/routers/skills.router.ts index d696b26..62eb961 100644 --- a/src/routers/skills.router.ts +++ b/src/routers/skills.router.ts @@ -10,7 +10,7 @@ skillsRouter.post( async (req: Request, res: Response) => { try { const skills = await createSkills(req.body); - res.status(HttpStatus.OK).json(event); + res.status(HttpStatus.OK).json(skills); } catch (err: unknown) { if (err instanceof HttpError) { res.status(err.errorCode).json({ error: err.message }); From ecaa947848f23a32f0bf2a968886cb71250b6197 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Sat, 24 Feb 2024 15:43:28 -0600 Subject: [PATCH 10/37] updates models to make them more structured --- src/models/activities.model.ts | 4 ++-- src/models/education.model.ts | 4 ++-- src/models/example_event_model.ts | 36 ------------------------------- src/models/experience.model.ts | 4 ++-- src/models/heading.model.ts | 35 ++++++++++++++++++++++-------- src/models/project.model.ts | 4 ++-- 6 files changed, 34 insertions(+), 53 deletions(-) delete mode 100644 src/models/example_event_model.ts diff --git a/src/models/activities.model.ts b/src/models/activities.model.ts index 451206e..29e9f11 100644 --- a/src/models/activities.model.ts +++ b/src/models/activities.model.ts @@ -4,7 +4,7 @@ const Schema = mongoose.Schema; // Interface for Activities document export interface ActivitiesType extends mongoose.Document { - description: string; + bullets: string[]; title: string; subtitle: string; year: string; @@ -13,7 +13,7 @@ export interface ActivitiesType extends mongoose.Document { // Activities Schema const Activities = new Schema({ - description: { type: String, required: true }, + bullets: { type: [String], required: true }, title: { type: String, required: true }, subtitle: { type: String, required: true }, year: { type: String, required: true }, diff --git a/src/models/education.model.ts b/src/models/education.model.ts index a2ff6f2..a7d12cb 100644 --- a/src/models/education.model.ts +++ b/src/models/education.model.ts @@ -4,7 +4,7 @@ const Schema = mongoose.Schema; // Interface for Education document export interface EducationType extends mongoose.Document { - description: string; + bullets: string[]; title: string; subtitle: string; location: string; @@ -13,7 +13,7 @@ export interface EducationType extends mongoose.Document { // Education Schema const Education = new Schema({ - description: { type: String, required: true }, + bullets: { type: [String], required: true }, title: { type: String, required: true }, subtitle: { type: String, required: true }, location: { type: String, required: true }, diff --git a/src/models/example_event_model.ts b/src/models/example_event_model.ts deleted file mode 100644 index ee38a40..0000000 --- a/src/models/example_event_model.ts +++ /dev/null @@ -1,36 +0,0 @@ -//this is a model from another project with comments added - -//get default import from mongoose library (which is used for interacting w/ mongodb) -import mongoose from "mongoose"; - -//mongoose object for defining structure of all the documents in a mongodb collection -const Schema = mongoose.Schema; - -//typescript type corresponding with the mongoose schema structure -export interface eventType { - name: string; - description: string; - code: string; - date: Date; - programs: string[]; - staff: string[]; - attended_youth?: string[]; - attached_forms?: string[]; -} - -//mongoose schema for an event document -const Event = new Schema({ - name: { type: String, required: true }, - description: { type: String, required: true }, - code: { type: String, required: true }, - date: { type: Date, required: true }, - programs: { type: [String], required: true }, - staff: { type: [String], required: true }, // by Staff.fireID - attended_youth: { type: [String], default: [] }, // by Youth.fireID - attached_forms: { type: [String], default: [] }, // by Note._id -}); - -//this takes the schema and uses it to form a model object. Model objects allow us -//to interact with mongodb, such as creating new documents or querying documents. The event -//model object is exported so that it can be used in other files -export const EventModel = mongoose.model("Event", Event); \ No newline at end of file diff --git a/src/models/experience.model.ts b/src/models/experience.model.ts index a1e1aa2..8fc9007 100644 --- a/src/models/experience.model.ts +++ b/src/models/experience.model.ts @@ -6,7 +6,7 @@ const Schema = mongoose.Schema; //typescript type corresponding with the mongoose schema structure export interface experienceType extends mongoose.Document { - description: string; + bullets: string[]; title: string; subtitle: string; date: string; @@ -15,7 +15,7 @@ export interface experienceType extends mongoose.Document { //mongoose schema for an event document const Experience = new Schema({ - description: { type: String, required: true }, + bullets: { type: [String], required: true }, title: { type: String, required: true }, subtitle: { type: String, required: true }, date: { type: String, required: true }, diff --git a/src/models/heading.model.ts b/src/models/heading.model.ts index f7c2f96..a7d0cbb 100644 --- a/src/models/heading.model.ts +++ b/src/models/heading.model.ts @@ -2,22 +2,39 @@ import mongoose from "mongoose"; const Schema = mongoose.Schema; +interface HeadingItem { + item: string; + href: string | null; +} + // Interface for Contact details document export interface HeadingType extends mongoose.Document { name: string; - phoneNumber: string; - email: string; - linkedIn: string; - websitesGitHub: string; + items: HeadingItem[]; } -// Contact Schema +// Schema for HeadingItem +const headingItemSchema = new Schema( + { + item: { type: String, required: true }, + href: { type: String, default: null }, // Assuming href can be null or a string + }, + { _id: false }, +); // _id is not needed for subdocuments by default unless you want them + +// Schema for HeadingType which includes HeadingItem const Heading = new Schema({ name: { type: String, required: true }, - phoneNumber: { type: String, required: true }, - email: { type: String, required: true }, - linkedIn: { type: String, required: true }, - websitesGitHub: { type: String, required: true }, + items: [headingItemSchema], }); +// Contact Schema +// const Heading = new Schema({ +// name: { type: String, required: true }, +// phoneNumber: { type: String, required: true }, +// email: { type: String, required: true }, +// linkedIn: { type: String, required: true }, +// websitesGitHub: { type: String, required: true }, +// }); + export const HeadingModel = mongoose.model("Heading", Heading); diff --git a/src/models/project.model.ts b/src/models/project.model.ts index 17fbcb8..b37361e 100644 --- a/src/models/project.model.ts +++ b/src/models/project.model.ts @@ -4,14 +4,14 @@ const Schema = mongoose.Schema; // Interface for Project document export interface ProjectType extends mongoose.Document { - description: string; + bullets: string[]; title: string; year: string; } // Project Schema const Project = new Schema({ - description: { type: String, required: true }, + bullets: { type: [String], required: true }, title: { type: String, required: true }, year: { type: String, required: true }, }); From 5b20413110acaf6072b4ddbad9be22e89d046097 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Sat, 24 Feb 2024 15:54:28 -0600 Subject: [PATCH 11/37] adds userid field to models --- src/models/activities.model.ts | 2 ++ src/models/education.model.ts | 2 ++ src/models/experience.model.ts | 2 ++ src/models/folder.model.ts | 2 ++ src/models/heading.model.ts | 2 ++ src/models/project.model.ts | 2 ++ src/models/resume.model.ts | 2 ++ src/models/skills.model.ts | 2 ++ 8 files changed, 16 insertions(+) diff --git a/src/models/activities.model.ts b/src/models/activities.model.ts index 29e9f11..52d69eb 100644 --- a/src/models/activities.model.ts +++ b/src/models/activities.model.ts @@ -4,6 +4,7 @@ const Schema = mongoose.Schema; // Interface for Activities document export interface ActivitiesType extends mongoose.Document { + userId: string; bullets: string[]; title: string; subtitle: string; @@ -13,6 +14,7 @@ export interface ActivitiesType extends mongoose.Document { // Activities Schema const Activities = new Schema({ + userId: { type: String, required: true }, bullets: { type: [String], required: true }, title: { type: String, required: true }, subtitle: { type: String, required: true }, diff --git a/src/models/education.model.ts b/src/models/education.model.ts index a7d12cb..7a22479 100644 --- a/src/models/education.model.ts +++ b/src/models/education.model.ts @@ -4,6 +4,7 @@ const Schema = mongoose.Schema; // Interface for Education document export interface EducationType extends mongoose.Document { + userId: string; bullets: string[]; title: string; subtitle: string; @@ -13,6 +14,7 @@ export interface EducationType extends mongoose.Document { // Education Schema const Education = new Schema({ + userId: { type: String, required: true }, bullets: { type: [String], required: true }, title: { type: String, required: true }, subtitle: { type: String, required: true }, diff --git a/src/models/experience.model.ts b/src/models/experience.model.ts index 8fc9007..8c4116c 100644 --- a/src/models/experience.model.ts +++ b/src/models/experience.model.ts @@ -6,6 +6,7 @@ const Schema = mongoose.Schema; //typescript type corresponding with the mongoose schema structure export interface experienceType extends mongoose.Document { + userId: string; bullets: string[]; title: string; subtitle: string; @@ -15,6 +16,7 @@ export interface experienceType extends mongoose.Document { //mongoose schema for an event document const Experience = new Schema({ + userId: { type: String, required: true }, bullets: { type: [String], required: true }, title: { type: String, required: true }, subtitle: { type: String, required: true }, diff --git a/src/models/folder.model.ts b/src/models/folder.model.ts index 68b2b94..e977194 100644 --- a/src/models/folder.model.ts +++ b/src/models/folder.model.ts @@ -5,12 +5,14 @@ const Schema = mongoose.Schema; //typescript type corresponding with the mongoose schema structure export interface folderType extends mongoose.Document { + userId: string; name: string; resumeIds: mongoose.Schema.Types.ObjectId[]; folderIds: mongoose.Schema.Types.ObjectId[]; } const Folder = new Schema({ + userId: { type: String, required: true }, name: { 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/models/heading.model.ts b/src/models/heading.model.ts index a7d0cbb..f4ed464 100644 --- a/src/models/heading.model.ts +++ b/src/models/heading.model.ts @@ -9,6 +9,7 @@ interface HeadingItem { // Interface for Contact details document export interface HeadingType extends mongoose.Document { + userId: string; name: string; items: HeadingItem[]; } @@ -24,6 +25,7 @@ const headingItemSchema = new Schema( // Schema for HeadingType which includes HeadingItem const Heading = new Schema({ + userId: { type: String, required: true }, name: { type: String, required: true }, items: [headingItemSchema], }); diff --git a/src/models/project.model.ts b/src/models/project.model.ts index b37361e..0183fdf 100644 --- a/src/models/project.model.ts +++ b/src/models/project.model.ts @@ -4,6 +4,7 @@ const Schema = mongoose.Schema; // Interface for Project document export interface ProjectType extends mongoose.Document { + userId: string; bullets: string[]; title: string; year: string; @@ -11,6 +12,7 @@ export interface ProjectType extends mongoose.Document { // Project Schema const Project = new Schema({ + userId: { type: String, required: true }, bullets: { type: [String], required: true }, title: { type: String, required: true }, year: { type: String, required: true }, diff --git a/src/models/resume.model.ts b/src/models/resume.model.ts index c8b63a7..e3ab449 100644 --- a/src/models/resume.model.ts +++ b/src/models/resume.model.ts @@ -5,11 +5,13 @@ const Schema = mongoose.Schema; //typescript type corresponding with the mongoose schema structure export interface resumeType extends mongoose.Document { + userId: string; itemIds: mongoose.Schema.Types.ObjectId[]; templateId: mongoose.Schema.Types.ObjectId; } const Resume = new Schema({ + userId: { type: String, required: true }, itemIds: { type: [Schema.Types.ObjectId], required: true }, templateId: { type: Schema.Types.ObjectId, required: true }, }); diff --git a/src/models/skills.model.ts b/src/models/skills.model.ts index 8c46b31..cabff04 100644 --- a/src/models/skills.model.ts +++ b/src/models/skills.model.ts @@ -4,12 +4,14 @@ const Schema = mongoose.Schema; // Interface for Skills document export interface SkillsType extends mongoose.Document { + userId: string; title: string; description: string; } // Skills Schema const Skills = new Schema({ + userId: { type: String, required: true }, title: { type: String, required: true }, description: { type: String, required: true }, }); From d73c0727f1b1ecc529f573e2b0cc4b4798b05f17 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Sat, 24 Feb 2024 15:57:02 -0600 Subject: [PATCH 12/37] adds itemName field to models --- src/models/activities.model.ts | 2 ++ src/models/education.model.ts | 2 ++ src/models/experience.model.ts | 2 ++ src/models/heading.model.ts | 2 ++ src/models/project.model.ts | 2 ++ src/models/resume.model.ts | 2 ++ src/models/skills.model.ts | 2 ++ 7 files changed, 14 insertions(+) diff --git a/src/models/activities.model.ts b/src/models/activities.model.ts index 52d69eb..52263ab 100644 --- a/src/models/activities.model.ts +++ b/src/models/activities.model.ts @@ -5,6 +5,7 @@ const Schema = mongoose.Schema; // Interface for Activities document export interface ActivitiesType extends mongoose.Document { userId: string; + itemName: string; bullets: string[]; title: string; subtitle: string; @@ -15,6 +16,7 @@ export interface ActivitiesType extends mongoose.Document { // Activities Schema const Activities = new Schema({ userId: { type: String, required: true }, + itemName: { type: String, required: true }, bullets: { type: [String], required: true }, title: { type: String, required: true }, subtitle: { type: String, required: true }, diff --git a/src/models/education.model.ts b/src/models/education.model.ts index 7a22479..89f454b 100644 --- a/src/models/education.model.ts +++ b/src/models/education.model.ts @@ -5,6 +5,7 @@ const Schema = mongoose.Schema; // Interface for Education document export interface EducationType extends mongoose.Document { userId: string; + itemName: string; bullets: string[]; title: string; subtitle: string; @@ -15,6 +16,7 @@ export interface EducationType extends mongoose.Document { // Education Schema const Education = new Schema({ userId: { type: String, required: true }, + itemName: { type: String, required: true }, bullets: { type: [String], required: true }, title: { type: String, required: true }, subtitle: { type: String, required: true }, diff --git a/src/models/experience.model.ts b/src/models/experience.model.ts index 8c4116c..4491f34 100644 --- a/src/models/experience.model.ts +++ b/src/models/experience.model.ts @@ -7,6 +7,7 @@ const Schema = mongoose.Schema; //typescript type corresponding with the mongoose schema structure export interface experienceType extends mongoose.Document { userId: string; + itemName: string; bullets: string[]; title: string; subtitle: string; @@ -17,6 +18,7 @@ export interface experienceType extends mongoose.Document { //mongoose schema for an event document const Experience = new Schema({ userId: { type: String, required: true }, + itemName: { type: String, required: true }, bullets: { type: [String], required: true }, title: { type: String, required: true }, subtitle: { type: String, required: true }, diff --git a/src/models/heading.model.ts b/src/models/heading.model.ts index f4ed464..c3822aa 100644 --- a/src/models/heading.model.ts +++ b/src/models/heading.model.ts @@ -10,6 +10,7 @@ interface HeadingItem { // Interface for Contact details document export interface HeadingType extends mongoose.Document { userId: string; + itemName: string; name: string; items: HeadingItem[]; } @@ -26,6 +27,7 @@ const headingItemSchema = new Schema( // Schema for HeadingType which includes HeadingItem const Heading = new Schema({ userId: { type: String, required: true }, + itemName: { type: String, required: true }, name: { type: String, required: true }, items: [headingItemSchema], }); diff --git a/src/models/project.model.ts b/src/models/project.model.ts index 0183fdf..c832981 100644 --- a/src/models/project.model.ts +++ b/src/models/project.model.ts @@ -5,6 +5,7 @@ const Schema = mongoose.Schema; // Interface for Project document export interface ProjectType extends mongoose.Document { userId: string; + itemName: string; bullets: string[]; title: string; year: string; @@ -13,6 +14,7 @@ export interface ProjectType extends mongoose.Document { // Project Schema const Project = new Schema({ userId: { type: String, required: true }, + itemName: { type: String, required: true }, bullets: { type: [String], required: true }, title: { type: String, required: true }, year: { type: String, required: true }, diff --git a/src/models/resume.model.ts b/src/models/resume.model.ts index e3ab449..3e8a9a0 100644 --- a/src/models/resume.model.ts +++ b/src/models/resume.model.ts @@ -6,12 +6,14 @@ const Schema = mongoose.Schema; //typescript type corresponding with the mongoose schema structure export interface resumeType extends mongoose.Document { userId: string; + itemName: string; itemIds: mongoose.Schema.Types.ObjectId[]; templateId: mongoose.Schema.Types.ObjectId; } const Resume = new Schema({ userId: { type: String, required: true }, + itemName: { type: String, required: true }, itemIds: { type: [Schema.Types.ObjectId], required: true }, templateId: { type: Schema.Types.ObjectId, required: true }, }); diff --git a/src/models/skills.model.ts b/src/models/skills.model.ts index cabff04..0f10390 100644 --- a/src/models/skills.model.ts +++ b/src/models/skills.model.ts @@ -5,6 +5,7 @@ const Schema = mongoose.Schema; // Interface for Skills document export interface SkillsType extends mongoose.Document { userId: string; + itemName: string; title: string; description: string; } @@ -12,6 +13,7 @@ export interface SkillsType extends mongoose.Document { // Skills Schema const Skills = new Schema({ userId: { type: String, required: true }, + itemName: { type: String, required: true }, title: { type: String, required: true }, description: { type: String, required: true }, }); From 942ed845ea63c3b52ab4b9a81929784821ac8c7c Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Sat, 24 Feb 2024 16:00:42 -0600 Subject: [PATCH 13/37] adds section heading model --- src/models/sectionHeading.model.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/models/sectionHeading.model.ts diff --git a/src/models/sectionHeading.model.ts b/src/models/sectionHeading.model.ts new file mode 100644 index 0000000..19338aa --- /dev/null +++ b/src/models/sectionHeading.model.ts @@ -0,0 +1,19 @@ +import mongoose from "mongoose"; + +const Schema = mongoose.Schema; + +// Interface for Section Heading document +export interface SectionHeadingType extends mongoose.Document { + userId: string; + itemName: string; + title: string; +} + +// Section Heading Schema +const SectionHeading = new Schema({ + userId: { type: String, required: true }, + itemName: { type: String, required: true }, + title: { type: String, required: true }, +}); + +export const SectionHeadingModel = mongoose.model("SectionHeading", SectionHeading); From a1e4fbda28994140bbc8e933b4f53ab155ab082a Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Sat, 24 Feb 2024 17:24:32 -0600 Subject: [PATCH 14/37] changed userId field to user for compatibility with verifyToken middleware --- src/models/activities.model.ts | 4 ++-- src/models/education.model.ts | 4 ++-- src/models/experience.model.ts | 4 ++-- src/models/folder.model.ts | 4 ++-- src/models/heading.model.ts | 4 ++-- src/models/project.model.ts | 4 ++-- src/models/resume.model.ts | 4 ++-- src/models/sectionHeading.model.ts | 4 ++-- src/models/skills.model.ts | 4 ++-- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/models/activities.model.ts b/src/models/activities.model.ts index 52263ab..36f8144 100644 --- a/src/models/activities.model.ts +++ b/src/models/activities.model.ts @@ -4,7 +4,7 @@ const Schema = mongoose.Schema; // Interface for Activities document export interface ActivitiesType extends mongoose.Document { - userId: string; + user: string; itemName: string; bullets: string[]; title: string; @@ -15,7 +15,7 @@ export interface ActivitiesType extends mongoose.Document { // Activities Schema const Activities = new Schema({ - userId: { type: String, required: true }, + user: { type: String, required: true }, itemName: { type: String, required: true }, bullets: { type: [String], required: true }, title: { type: String, required: true }, diff --git a/src/models/education.model.ts b/src/models/education.model.ts index 89f454b..f2b42a3 100644 --- a/src/models/education.model.ts +++ b/src/models/education.model.ts @@ -4,7 +4,7 @@ const Schema = mongoose.Schema; // Interface for Education document export interface EducationType extends mongoose.Document { - userId: string; + user: string; itemName: string; bullets: string[]; title: string; @@ -15,7 +15,7 @@ export interface EducationType extends mongoose.Document { // Education Schema const Education = new Schema({ - userId: { type: String, required: true }, + user: { type: String, required: true }, itemName: { type: String, required: true }, bullets: { type: [String], required: true }, title: { type: String, required: true }, diff --git a/src/models/experience.model.ts b/src/models/experience.model.ts index 4491f34..8fe7908 100644 --- a/src/models/experience.model.ts +++ b/src/models/experience.model.ts @@ -6,7 +6,7 @@ const Schema = mongoose.Schema; //typescript type corresponding with the mongoose schema structure export interface experienceType extends mongoose.Document { - userId: string; + user: string; itemName: string; bullets: string[]; title: string; @@ -17,7 +17,7 @@ export interface experienceType extends mongoose.Document { //mongoose schema for an event document const Experience = new Schema({ - userId: { type: String, required: true }, + user: { type: String, required: true }, itemName: { type: String, required: true }, bullets: { type: [String], required: true }, title: { type: String, required: true }, diff --git a/src/models/folder.model.ts b/src/models/folder.model.ts index e977194..e9414ce 100644 --- a/src/models/folder.model.ts +++ b/src/models/folder.model.ts @@ -5,14 +5,14 @@ const Schema = mongoose.Schema; //typescript type corresponding with the mongoose schema structure export interface folderType extends mongoose.Document { - userId: string; + user: string; name: string; resumeIds: mongoose.Schema.Types.ObjectId[]; folderIds: mongoose.Schema.Types.ObjectId[]; } const Folder = new Schema({ - userId: { type: String, required: true }, + user: { type: String, required: true }, name: { 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/models/heading.model.ts b/src/models/heading.model.ts index c3822aa..56e5f03 100644 --- a/src/models/heading.model.ts +++ b/src/models/heading.model.ts @@ -9,7 +9,7 @@ interface HeadingItem { // Interface for Contact details document export interface HeadingType extends mongoose.Document { - userId: string; + user: string; itemName: string; name: string; items: HeadingItem[]; @@ -26,7 +26,7 @@ const headingItemSchema = new Schema( // Schema for HeadingType which includes HeadingItem const Heading = new Schema({ - userId: { type: String, required: true }, + user: { type: String, required: true }, itemName: { type: String, required: true }, name: { type: String, required: true }, items: [headingItemSchema], diff --git a/src/models/project.model.ts b/src/models/project.model.ts index c832981..784d78f 100644 --- a/src/models/project.model.ts +++ b/src/models/project.model.ts @@ -4,7 +4,7 @@ const Schema = mongoose.Schema; // Interface for Project document export interface ProjectType extends mongoose.Document { - userId: string; + user: string; itemName: string; bullets: string[]; title: string; @@ -13,7 +13,7 @@ export interface ProjectType extends mongoose.Document { // Project Schema const Project = new Schema({ - userId: { type: String, required: true }, + user: { type: String, required: true }, itemName: { type: String, required: true }, bullets: { type: [String], required: true }, title: { type: String, required: true }, diff --git a/src/models/resume.model.ts b/src/models/resume.model.ts index 3e8a9a0..2f2682b 100644 --- a/src/models/resume.model.ts +++ b/src/models/resume.model.ts @@ -5,14 +5,14 @@ const Schema = mongoose.Schema; //typescript type corresponding with the mongoose schema structure export interface resumeType extends mongoose.Document { - userId: string; + user: string; itemName: string; itemIds: mongoose.Schema.Types.ObjectId[]; templateId: mongoose.Schema.Types.ObjectId; } const Resume = new Schema({ - userId: { type: String, required: true }, + user: { type: String, required: true }, itemName: { type: String, required: true }, itemIds: { type: [Schema.Types.ObjectId], required: true }, templateId: { type: Schema.Types.ObjectId, required: true }, diff --git a/src/models/sectionHeading.model.ts b/src/models/sectionHeading.model.ts index 19338aa..c6d8391 100644 --- a/src/models/sectionHeading.model.ts +++ b/src/models/sectionHeading.model.ts @@ -4,14 +4,14 @@ const Schema = mongoose.Schema; // Interface for Section Heading document export interface SectionHeadingType extends mongoose.Document { - userId: string; + user: string; itemName: string; title: string; } // Section Heading Schema const SectionHeading = new Schema({ - userId: { type: String, required: true }, + user: { type: String, required: true }, itemName: { type: String, required: true }, title: { type: String, required: true }, }); diff --git a/src/models/skills.model.ts b/src/models/skills.model.ts index 0f10390..32be2a8 100644 --- a/src/models/skills.model.ts +++ b/src/models/skills.model.ts @@ -4,7 +4,7 @@ const Schema = mongoose.Schema; // Interface for Skills document export interface SkillsType extends mongoose.Document { - userId: string; + user: string; itemName: string; title: string; description: string; @@ -12,7 +12,7 @@ export interface SkillsType extends mongoose.Document { // Skills Schema const Skills = new Schema({ - userId: { type: String, required: true }, + user: { type: String, required: true }, itemName: { type: String, required: true }, title: { type: String, required: true }, description: { type: String, required: true }, From fbd95899e81747b10a27cbc7d7fb922f36facec8 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Sat, 24 Feb 2024 18:14:47 -0600 Subject: [PATCH 15/37] adds activity post route --- src/controllers/activities.controller.ts | 30 ++++++++++++++++++++++++ src/routers/activities.router.ts | 25 ++++++++++++++++++++ src/routers/root.router.ts | 2 ++ 3 files changed, 57 insertions(+) create mode 100644 src/controllers/activities.controller.ts create mode 100644 src/routers/activities.router.ts diff --git a/src/controllers/activities.controller.ts b/src/controllers/activities.controller.ts new file mode 100644 index 0000000..73deeef --- /dev/null +++ b/src/controllers/activities.controller.ts @@ -0,0 +1,30 @@ +import { ActivitiesModel, type ActivitiesType } from "../models/activities.model"; +import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; +import { checkDuplicateItemName } from "../utils/checkDuplicates"; + +export const createActivities = async (activitiesFields: ActivitiesType) => { + try { + if(await checkDuplicateItemName(activitiesFields.itemName)){ + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Duplicate item name", + ) + } + + const newActivities = new ActivitiesModel(activitiesFields); + await newActivities.save(); + return newActivities; + } catch (err: unknown) { + if (err instanceof HttpError) { + throw err; + } + + checkMongooseErrors(err); + + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "Activity creation failed", + { cause: err }, + ); + } +}; diff --git a/src/routers/activities.router.ts b/src/routers/activities.router.ts new file mode 100644 index 0000000..45f1d5b --- /dev/null +++ b/src/routers/activities.router.ts @@ -0,0 +1,25 @@ +import { Router, type Request, type Response } from "express"; +import { createActivities } from "../controllers/activities.controller"; +import { HttpError, HttpStatus } from "../utils/errors"; +import { type ActivitiesType } from "../models/activities.model"; + +export const activitiesRouter = Router(); + +//Note that the user field (which is part of activitiesType) in body is automatically populated by verifyToken middleware +activitiesRouter.post( + "/", + async (req: Request, res: Response) => { + try { + const activities = await createActivities(req.body); + res.status(HttpStatus.OK).json(activities); + } 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/routers/root.router.ts b/src/routers/root.router.ts index cdef0f7..a5ef2c8 100644 --- a/src/routers/root.router.ts +++ b/src/routers/root.router.ts @@ -1,6 +1,8 @@ import { Router } from "express"; import { skillsRouter } from "./skills.router"; +import { activitiesRouter } from "./activities.router"; export const router = Router(); router.use("/skills", skillsRouter); +router.use("/activities", activitiesRouter); From b86aed9dbf146834d90c87885266fec69dd66d84 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Sat, 24 Feb 2024 18:15:36 -0600 Subject: [PATCH 16/37] updates skills route to avoid duplicates --- src/controllers/skills.controller.ts | 10 +++++++- src/routers/skills.router.ts | 1 + src/utils/checkDuplicates.ts | 35 ++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 src/utils/checkDuplicates.ts diff --git a/src/controllers/skills.controller.ts b/src/controllers/skills.controller.ts index a2f3fce..a8cd3be 100644 --- a/src/controllers/skills.controller.ts +++ b/src/controllers/skills.controller.ts @@ -1,8 +1,16 @@ import { SkillsModel, type SkillsType } from "../models/skills.model"; import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; +import { checkDuplicateItemName } from "../utils/checkDuplicates"; export const createSkills = async (skillsFields: SkillsType) => { try { + if(await checkDuplicateItemName(skillsFields.itemName)){ + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Duplicate item name", + ) + } + const newSkills = new SkillsModel(skillsFields); await newSkills.save(); return newSkills; @@ -15,7 +23,7 @@ export const createSkills = async (skillsFields: SkillsType) => { throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, - "Event creation failed", + "Skill creation failed", { cause: err }, ); } diff --git a/src/routers/skills.router.ts b/src/routers/skills.router.ts index 62eb961..5c0edf2 100644 --- a/src/routers/skills.router.ts +++ b/src/routers/skills.router.ts @@ -5,6 +5,7 @@ import { type SkillsType } from "../models/skills.model"; export const skillsRouter = Router(); +//Note that the user field (which is part of skillsType) in body is automatically populated by verifyToken middleware skillsRouter.post( "/", async (req: Request, res: Response) => { diff --git a/src/utils/checkDuplicates.ts b/src/utils/checkDuplicates.ts new file mode 100644 index 0000000..e713aec --- /dev/null +++ b/src/utils/checkDuplicates.ts @@ -0,0 +1,35 @@ +import mongoose from "mongoose"; +import { ActivitiesModel } from "../models/activities.model"; +import { EducationModel } from "../models/education.model"; +import { ExperienceModel } from "../models/experience.model"; +import { HeadingModel } from "../models/heading.model"; +import { ProjectModel } from "../models/project.model"; +import { SectionHeadingModel } from "../models/sectionHeading.model"; +import { SkillsModel } from "../models/skills.model"; + +export const checkDuplicateItemName = async (value: string): Promise => { + const field = "itemName"; + const models = [ + ActivitiesModel, + EducationModel, + ExperienceModel, + HeadingModel, + ProjectModel, + SectionHeadingModel, + SkillsModel, + ]; + + // Check each model for the count of documents with the specified itemName value + const checks = models.map((model) => + model.countDocuments({ [field]: value }).exec(), + ); + + // Await all checks to resolve + const results = await Promise.all(checks); + + // Sum the counts from all models + const totalDuplicates = results.reduce((acc, count) => acc + count, 0); + + // If totalDuplicates is greater than 0, a duplicate exists + return totalDuplicates > 0; +}; From 6b42b5cb29aec3a5ccef87d9ac64dc60309f2d11 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Sat, 24 Feb 2024 18:16:05 -0600 Subject: [PATCH 17/37] adds controllers and modifies education model --- src/controllers/.gitkeep | 1 - src/controllers/education.controller.ts | 0 src/controllers/experience.controller.ts | 0 src/controllers/folder.controller.ts | 0 src/controllers/heading.controller.ts | 0 src/controllers/project.controller.ts | 0 src/controllers/resume.controller.ts | 0 src/index.ts | 2 +- src/models/education.model.ts | 2 +- 9 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 src/controllers/.gitkeep create mode 100644 src/controllers/education.controller.ts create mode 100644 src/controllers/experience.controller.ts create mode 100644 src/controllers/folder.controller.ts create mode 100644 src/controllers/heading.controller.ts create mode 100644 src/controllers/project.controller.ts create mode 100644 src/controllers/resume.controller.ts diff --git a/src/controllers/.gitkeep b/src/controllers/.gitkeep deleted file mode 100644 index 380ec71..0000000 --- a/src/controllers/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -This is just here so the folder is tracked by git \ No newline at end of file diff --git a/src/controllers/education.controller.ts b/src/controllers/education.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/experience.controller.ts b/src/controllers/experience.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/folder.controller.ts b/src/controllers/folder.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/heading.controller.ts b/src/controllers/heading.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/project.controller.ts b/src/controllers/project.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/resume.controller.ts b/src/controllers/resume.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/index.ts b/src/index.ts index 0a5b9d3..429445f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,7 +25,7 @@ app.use(helmet()); * Uses the verifyToken middleware to protect the "/data" route * Use the verifyToken to protect all the routes that require authentication */ -app.use("/api", verifyToken, router); +app.use("/api", /*verifyToken,*/ router); app.use("/example", verifyToken, exampleRoute); // Default route: Unprotected diff --git a/src/models/education.model.ts b/src/models/education.model.ts index f2b42a3..1020532 100644 --- a/src/models/education.model.ts +++ b/src/models/education.model.ts @@ -24,4 +24,4 @@ const Education = new Schema({ year: { type: String, required: true }, }); -export const EducationModel = mongoose.model("Education", Education); +export const EducationModel = mongoose.model("Education", Education, "education"); From c2fd3d0d4ff68c9deedb2ff9047678d19bc92d81 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Sat, 24 Feb 2024 19:12:30 -0600 Subject: [PATCH 18/37] adds test for activities controller functions --- src/tests/controllers.tests/activities.test.ts | 17 +++++++++++++++++ src/tests/controllers.tests/dummyData.ts | 9 +++++++++ src/tests/dbHandler.ts | 16 ++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 src/tests/controllers.tests/activities.test.ts create mode 100644 src/tests/controllers.tests/dummyData.ts create mode 100644 src/tests/dbHandler.ts diff --git a/src/tests/controllers.tests/activities.test.ts b/src/tests/controllers.tests/activities.test.ts new file mode 100644 index 0000000..81f7af8 --- /dev/null +++ b/src/tests/controllers.tests/activities.test.ts @@ -0,0 +1,17 @@ +import { dbConnect, dbDisconnect } from "../dbHandler"; +import { ActivitiesType } from "../../models/activities.model"; +import { activityDummyData1 } from "./dummyData"; +import { createActivity, getAllActivities } from "../../controllers/activities.controller"; +import { describe, test, expect, beforeEach, afterEach } from "vitest"; + +describe("Activities controller tests", () => { + beforeEach(async () => dbConnect()); + afterEach(async () => dbDisconnect()); + + test("Adds and retrieves an activity", async () => { + await createActivity(activityDummyData1 as ActivitiesType); + const returnedActivities = await getAllActivities(activityDummyData1.user); + expect(returnedActivities.length).to.equal(1); + expect(returnedActivities[0]).toMatchObject(activityDummyData1); + }) +}) \ No newline at end of file diff --git a/src/tests/controllers.tests/dummyData.ts b/src/tests/controllers.tests/dummyData.ts new file mode 100644 index 0000000..ef8aff8 --- /dev/null +++ b/src/tests/controllers.tests/dummyData.ts @@ -0,0 +1,9 @@ +export const activityDummyData1 = { + user: "test", + itemName: "activitesItem1", + bullets: ["example bullet"], + title: "title", + subtitle: "subtitle", + year: "year", + location: "location", +}; diff --git a/src/tests/dbHandler.ts b/src/tests/dbHandler.ts new file mode 100644 index 0000000..ff40f59 --- /dev/null +++ b/src/tests/dbHandler.ts @@ -0,0 +1,16 @@ +import mongoose from "mongoose"; +import { MongoMemoryServer } from 'mongodb-memory-server'; + +let mongoServer: MongoMemoryServer; + +export const dbConnect = async () => { + mongoServer = await MongoMemoryServer.create(); + const uri = mongoServer.getUri(); + await mongoose.connect(uri); +}; + +export const dbDisconnect = async () => { + await mongoose.connection.dropDatabase(); + await mongoose.connection.close(); + await mongoServer.stop(); +}; \ No newline at end of file From 386d27e6e62f4ca85e9f23cc5bb6b42362ab36b4 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Sat, 24 Feb 2024 19:12:51 -0600 Subject: [PATCH 19/37] updates activities controller --- package-lock.json | 472 +++++++++++++++++++++++ package.json | 1 + src/controllers/activities.controller.ts | 22 +- src/routers/activities.router.ts | 4 +- 4 files changed, 496 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6da1fd9..994e303 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "eslint-plugin-n": "^16.1.0", "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-promise": "^6.1.1", + "mongodb-memory-server": "^9.1.6", "nodemon": "^3.0.1", "prettier": "^3.0.3", "ts-node": "^10.9.1", @@ -1881,6 +1882,15 @@ "node": "*" } }, + "node_modules/async-mutex": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "dev": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/async-retry": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", @@ -1902,12 +1912,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "devOptional": true }, + "node_modules/bare-events": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.0.tgz", + "integrity": "sha512-Yyyqff4PIFfSuthCZqLlPISTWHmnQxoPuAvkmgzsJEmG3CesdIv6Xweayl0JkCZJSB2yYIdJyEz97tpxNhgjbg==", + "dev": true, + "optional": true + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2055,6 +2078,15 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -2122,6 +2154,18 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/catharsis": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", @@ -2251,6 +2295,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "devOptional": true }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -3434,6 +3484,12 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -3522,6 +3578,15 @@ "node": ">=0.8.0" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3576,6 +3641,23 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3633,6 +3715,26 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -4265,6 +4367,19 @@ "node": ">= 0.4" } }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "devOptional": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -4645,6 +4760,12 @@ "xmlcreate": "^2.0.4" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "devOptional": true + }, "node_modules/jsdoc": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz", @@ -4976,6 +5097,30 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", "dev": true }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -5249,6 +5394,172 @@ "node": ">=16" } }, + "node_modules/mongodb-memory-server": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-9.1.6.tgz", + "integrity": "sha512-gzcpgGYlPhuKmria37W+bvYy6W+OkX2UVG7MoP41OWFvQv2Hn7A+fLXkV+lsMmhog1lMQprdV6AR+gixgheLaw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "mongodb-memory-server-core": "9.1.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongodb-memory-server-core": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-9.1.6.tgz", + "integrity": "sha512-3H/dq5II+XcSbK80hicMw4zFlDxcpjt4oWJq76RlOVuLoaf3AFqVheR6Vqx9ymlIqER4Jni58FMCIIRbesia1A==", + "dev": true, + "dependencies": { + "async-mutex": "^0.4.0", + "camelcase": "^6.3.0", + "debug": "^4.3.4", + "find-cache-dir": "^3.3.2", + "follow-redirects": "^1.15.3", + "https-proxy-agent": "^7.0.2", + "mongodb": "^5.9.1", + "new-find-package-json": "^2.0.0", + "semver": "^7.5.4", + "tar-stream": "^3.0.0", + "tslib": "^2.6.2", + "yauzl": "^2.10.0" + }, + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/bson": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", + "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==", + "dev": true, + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/mongodb": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", + "integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==", + "dev": true, + "dependencies": { + "bson": "^5.5.0", + "mongodb-connection-string-url": "^2.6.0", + "socks": "^2.7.1" + }, + "engines": { + "node": ">=14.20.1" + }, + "optionalDependencies": { + "@mongodb-js/saslprep": "^1.1.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.0.0", + "kerberos": "^1.0.0 || ^2.0.0", + "mongodb-client-encryption": ">=2.3.0 <3", + "snappy": "^7.2.2" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + } + } + }, + "node_modules/mongodb-memory-server-core/node_modules/mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "dev": true, + "dependencies": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/mongoose": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.1.1.tgz", @@ -5370,6 +5681,18 @@ "node": ">= 0.6" } }, + "node_modules/new-find-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz", + "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">=12.22.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -5698,6 +6021,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5780,6 +6112,12 @@ "node": "*" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -5798,6 +6136,70 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/pkg-types": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", @@ -6068,6 +6470,12 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -6541,6 +6949,30 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "devOptional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.1.tgz", + "integrity": "sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==", + "devOptional": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6567,6 +6999,12 @@ "memory-pager": "^1.0.2" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "devOptional": true + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -6602,6 +7040,19 @@ "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", "optional": true }, + "node_modules/streamx": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -6767,6 +7218,17 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/teeny-request": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.3.tgz", @@ -7667,6 +8129,16 @@ "node": ">=12" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 27a23b8..23f5fde 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "eslint-plugin-n": "^16.1.0", "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-promise": "^6.1.1", + "mongodb-memory-server": "^9.1.6", "nodemon": "^3.0.1", "prettier": "^3.0.3", "ts-node": "^10.9.1", diff --git a/src/controllers/activities.controller.ts b/src/controllers/activities.controller.ts index 73deeef..0d06391 100644 --- a/src/controllers/activities.controller.ts +++ b/src/controllers/activities.controller.ts @@ -2,7 +2,7 @@ import { ActivitiesModel, type ActivitiesType } from "../models/activities.model import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; import { checkDuplicateItemName } from "../utils/checkDuplicates"; -export const createActivities = async (activitiesFields: ActivitiesType) => { +export const createActivity = async (activitiesFields: ActivitiesType) => { try { if(await checkDuplicateItemName(activitiesFields.itemName)){ throw new HttpError( @@ -28,3 +28,23 @@ export const createActivities = async (activitiesFields: ActivitiesType) => { ); } }; + +export const getAllActivities = async (user: string) => { + try { + const activities = await ActivitiesModel.find({ user: user }); + return activities; + } 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, + "Activities retrieval failed", + { cause: err }, + ); + } +} \ No newline at end of file diff --git a/src/routers/activities.router.ts b/src/routers/activities.router.ts index 45f1d5b..fa9a7b5 100644 --- a/src/routers/activities.router.ts +++ b/src/routers/activities.router.ts @@ -1,5 +1,5 @@ import { Router, type Request, type Response } from "express"; -import { createActivities } from "../controllers/activities.controller"; +import { createActivity } from "../controllers/activities.controller"; import { HttpError, HttpStatus } from "../utils/errors"; import { type ActivitiesType } from "../models/activities.model"; @@ -10,7 +10,7 @@ activitiesRouter.post( "/", async (req: Request, res: Response) => { try { - const activities = await createActivities(req.body); + const activities = await createActivity(req.body); res.status(HttpStatus.OK).json(activities); } catch (err: unknown) { if (err instanceof HttpError) { From c82bd613672d51584b98280d9e49756020664a19 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Sun, 25 Feb 2024 13:38:54 -0600 Subject: [PATCH 20/37] adds tests --- package.json | 2 +- src/tests/controllers.tests/activities.test.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 23f5fde..f62bbe6 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "nodemon src/index.ts", "build": "tsc", - "test": "vitest", + "test": "vitest run", "lint": "eslint --ext .ts . --fix", "typecheck": "tsc --noEmit" }, diff --git a/src/tests/controllers.tests/activities.test.ts b/src/tests/controllers.tests/activities.test.ts index 81f7af8..4c2bd08 100644 --- a/src/tests/controllers.tests/activities.test.ts +++ b/src/tests/controllers.tests/activities.test.ts @@ -11,7 +11,22 @@ describe("Activities controller tests", () => { test("Adds and retrieves an activity", async () => { await createActivity(activityDummyData1 as ActivitiesType); const returnedActivities = await getAllActivities(activityDummyData1.user); + + //get back the 1 activity that was added expect(returnedActivities.length).to.equal(1); expect(returnedActivities[0]).toMatchObject(activityDummyData1); + + //Can't add duplicate name + await expect(createActivity(activityDummyData1 as ActivitiesType)).rejects.toThrowError(); + + const returnedActivities2 = await getAllActivities(activityDummyData1.user); + + //if duplicate, shouldn't add to db + expect(returnedActivities2.length).to.equal(1); + + const returnedActivities3 = await getAllActivities("fakeuserid"); + + //don't get records for a different user id + expect(returnedActivities3.length).to.equal(0); }) }) \ No newline at end of file From 3393640cb2a16edaf6a472f8c61fd6cbb180716b Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Tue, 27 Feb 2024 20:35:51 -0600 Subject: [PATCH 21/37] implements all activities controller functions and their tests --- src/controllers/activities.controller.ts | 105 ++++++++++++++++-- .../controllers.tests/activities.test.ts | 73 +++++++++--- 2 files changed, 151 insertions(+), 27 deletions(-) diff --git a/src/controllers/activities.controller.ts b/src/controllers/activities.controller.ts index 0d06391..1b3d1f2 100644 --- a/src/controllers/activities.controller.ts +++ b/src/controllers/activities.controller.ts @@ -1,15 +1,15 @@ -import { ActivitiesModel, type ActivitiesType } from "../models/activities.model"; +import { + ActivitiesModel, + type ActivitiesType, +} from "../models/activities.model"; import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; import { checkDuplicateItemName } from "../utils/checkDuplicates"; export const createActivity = async (activitiesFields: ActivitiesType) => { try { - if(await checkDuplicateItemName(activitiesFields.itemName)){ - throw new HttpError( - HttpStatus.BAD_REQUEST, - "Duplicate item name", - ) - } + if (await checkDuplicateItemName(activitiesFields.itemName)) { + throw new HttpError(HttpStatus.BAD_REQUEST, "Duplicate item name"); + } const newActivities = new ActivitiesModel(activitiesFields); await newActivities.save(); @@ -30,7 +30,7 @@ export const createActivity = async (activitiesFields: ActivitiesType) => { }; export const getAllActivities = async (user: string) => { - try { + try { const activities = await ActivitiesModel.find({ user: user }); return activities; } catch (err: unknown) { @@ -47,4 +47,91 @@ export const getAllActivities = async (user: string) => { { cause: err }, ); } -} \ No newline at end of file +}; + +export const getActivityById = async (user: string, activityId: string) => { + try { + const activities = await ActivitiesModel.findOne({ + user: user, + _id: activityId, + }); + return activities; + } 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, + "Activity retrieval failed", + { cause: err }, + ); + } +}; + +export const updateActivity = async ( + user: string, + activitiesFields: ActivitiesType, +) => { + try { + const _id = activitiesFields._id; // Extract the _id from activitiesFields + if (!_id) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Missing activity ID for update", + ); + } + + const updatedActivity = await ActivitiesModel.findOneAndUpdate( + { _id: _id, user: user }, // Query to match the document by _id and user + { $set: activitiesFields }, // Update operation + { new: true, runValidators: true }, // Options: return the updated document and run schema validators + ); + return updatedActivity; + } 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, + "Activity update failed", + { cause: err }, + ); + } +}; + +export const deleteActivity = async (user: string, activityId: string) => { + try { + const deletedActivity = await ActivitiesModel.findOneAndDelete({ + _id: activityId, + user: user, + }); + if (!deletedActivity) { + throw new HttpError( + HttpStatus.NOT_FOUND, + "Activity not found or already deleted", + ); + } + return { message: "Activity 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, + "Activity deletion failed", + { cause: err }, + ); + } +}; diff --git a/src/tests/controllers.tests/activities.test.ts b/src/tests/controllers.tests/activities.test.ts index 4c2bd08..7dd3c44 100644 --- a/src/tests/controllers.tests/activities.test.ts +++ b/src/tests/controllers.tests/activities.test.ts @@ -1,32 +1,69 @@ import { dbConnect, dbDisconnect } from "../dbHandler"; import { ActivitiesType } from "../../models/activities.model"; import { activityDummyData1 } from "./dummyData"; -import { createActivity, getAllActivities } from "../../controllers/activities.controller"; +import { + createActivity, + getAllActivities, + getActivityById, + updateActivity, + deleteActivity, +} from "../../controllers/activities.controller"; import { describe, test, expect, beforeEach, afterEach } from "vitest"; describe("Activities controller tests", () => { - beforeEach(async () => dbConnect()); - afterEach(async () => dbDisconnect()); + beforeEach(async () => dbConnect()); + afterEach(async () => dbDisconnect()); - test("Adds and retrieves an activity", async () => { - await createActivity(activityDummyData1 as ActivitiesType); - const returnedActivities = await getAllActivities(activityDummyData1.user); + test("Adds and retrieves an activity", async () => { + await createActivity(activityDummyData1 as ActivitiesType); + const returnedActivities = await getAllActivities(activityDummyData1.user); - //get back the 1 activity that was added - expect(returnedActivities.length).to.equal(1); - expect(returnedActivities[0]).toMatchObject(activityDummyData1); + //get back the 1 activity that was added + expect(returnedActivities.length).to.equal(1); + expect(returnedActivities[0]).toMatchObject(activityDummyData1); - //Can't add duplicate name - await expect(createActivity(activityDummyData1 as ActivitiesType)).rejects.toThrowError(); + //Can't add duplicate name + await expect( + createActivity(activityDummyData1 as ActivitiesType), + ).rejects.toThrowError(); - const returnedActivities2 = await getAllActivities(activityDummyData1.user); + const returnedActivities2 = await getAllActivities(activityDummyData1.user); - //if duplicate, shouldn't add to db - expect(returnedActivities2.length).to.equal(1); + //if duplicate, shouldn't add to db + expect(returnedActivities2.length).to.equal(1); - const returnedActivities3 = await getAllActivities("fakeuserid"); + const returnedActivities3 = await getAllActivities("fakeuserid"); - //don't get records for a different user id + //don't get records for a different user id + expect(returnedActivities3.length).to.equal(0); + }); + + test("Finds, updates, and deletes an activity", async () => { + await createActivity(activityDummyData1 as ActivitiesType); + const returnedActivities = await getAllActivities(activityDummyData1.user); + + const returnedActivity = await getActivityById( + activityDummyData1.user, + returnedActivities[0]._id, + ); + + expect(returnedActivity).toMatchObject(activityDummyData1); + + await updateActivity(activityDummyData1.user, { + ...activityDummyData1, + _id: returnedActivities[0]._id, + itemName: "activitiesItem2", + } as ActivitiesType); + const returnedActivity2 = await getActivityById( + activityDummyData1.user, + returnedActivities[0]._id, + ); + expect(returnedActivity2?.itemName).to.equal("activitiesItem2"); + + await deleteActivity(activityDummyData1.user, returnedActivities[0]._id); + const returnedActivities3 = await getAllActivities(activityDummyData1.user); expect(returnedActivities3.length).to.equal(0); - }) -}) \ No newline at end of file + + await expect(updateActivity(activityDummyData1.user, {} as ActivitiesType)).rejects.toThrowError("Missing"); + }); +}); From 1f314f26eb6ce8bd88fca682ba980486ae3e99be Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Tue, 27 Feb 2024 22:12:14 -0600 Subject: [PATCH 22/37] fully implements activities backend --- src/controllers/activities.controller.ts | 12 +-- src/index.ts | 2 +- src/routers/activities.router.ts | 82 ++++++++++++++++++- .../controllers.tests/activities.test.ts | 11 +-- 4 files changed, 93 insertions(+), 14 deletions(-) diff --git a/src/controllers/activities.controller.ts b/src/controllers/activities.controller.ts index 1b3d1f2..11d9c44 100644 --- a/src/controllers/activities.controller.ts +++ b/src/controllers/activities.controller.ts @@ -11,9 +11,9 @@ export const createActivity = async (activitiesFields: ActivitiesType) => { throw new HttpError(HttpStatus.BAD_REQUEST, "Duplicate item name"); } - const newActivities = new ActivitiesModel(activitiesFields); - await newActivities.save(); - return newActivities; + const newActivity = new ActivitiesModel(activitiesFields); + await newActivity.save(); + return newActivity; } catch (err: unknown) { if (err instanceof HttpError) { throw err; @@ -74,11 +74,11 @@ export const getActivityById = async (user: string, activityId: string) => { export const updateActivity = async ( user: string, + activityId: string, activitiesFields: ActivitiesType, ) => { try { - const _id = activitiesFields._id; // Extract the _id from activitiesFields - if (!_id) { + if (!activityId) { throw new HttpError( HttpStatus.BAD_REQUEST, "Missing activity ID for update", @@ -86,7 +86,7 @@ export const updateActivity = async ( } const updatedActivity = await ActivitiesModel.findOneAndUpdate( - { _id: _id, user: user }, // Query to match the document by _id and user + { _id: activityId, user: user }, // Query to match the document by _id and user { $set: activitiesFields }, // Update operation { new: true, runValidators: true }, // Options: return the updated document and run schema validators ); diff --git a/src/index.ts b/src/index.ts index 429445f..0a5b9d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,7 +25,7 @@ app.use(helmet()); * Uses the verifyToken middleware to protect the "/data" route * Use the verifyToken to protect all the routes that require authentication */ -app.use("/api", /*verifyToken,*/ router); +app.use("/api", verifyToken, router); app.use("/example", verifyToken, exampleRoute); // Default route: Unprotected diff --git a/src/routers/activities.router.ts b/src/routers/activities.router.ts index fa9a7b5..f11e3ac 100644 --- a/src/routers/activities.router.ts +++ b/src/routers/activities.router.ts @@ -1,5 +1,11 @@ import { Router, type Request, type Response } from "express"; -import { createActivity } from "../controllers/activities.controller"; +import { + createActivity, + getAllActivities, + getActivityById, + updateActivity, + deleteActivity, +} from "../controllers/activities.controller"; import { HttpError, HttpStatus } from "../utils/errors"; import { type ActivitiesType } from "../models/activities.model"; @@ -10,7 +16,25 @@ activitiesRouter.post( "/", async (req: Request, res: Response) => { try { - const activities = await createActivity(req.body); + const activity = await createActivity(req.body); + res.status(HttpStatus.OK).json(activity); + } 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" }); + } + } + }, +); + +activitiesRouter.get( + "/", + async (req: Request, res: Response) => { + try { + const activities = await getAllActivities(req.body.user); res.status(HttpStatus.OK).json(activities); } catch (err: unknown) { if (err instanceof HttpError) { @@ -23,3 +47,57 @@ activitiesRouter.post( } }, ); + +activitiesRouter.get( + "/:activityId", + async (req: Request, res: Response) => { + try { + const activity = await getActivityById(req.body.user, req.params.activityId); + res.status(HttpStatus.OK).json(activity); + } 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" }); + } + } + }, +); + +activitiesRouter.put( + "/:activityId", + async (req: Request, res: Response) => { + try { + const activity = await updateActivity(req.body.user, req.params.activityId, req.body); + res.status(HttpStatus.OK).json(activity); + } 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" }); + } + } + }, +); + +activitiesRouter.delete( + "/:activityId", + async (req: Request, res: Response) => { + try { + const activity = await deleteActivity(req.body.user, req.params.activityId); + res.status(HttpStatus.OK).json(activity); + } 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/activities.test.ts b/src/tests/controllers.tests/activities.test.ts index 7dd3c44..fa6b2eb 100644 --- a/src/tests/controllers.tests/activities.test.ts +++ b/src/tests/controllers.tests/activities.test.ts @@ -49,9 +49,8 @@ describe("Activities controller tests", () => { expect(returnedActivity).toMatchObject(activityDummyData1); - await updateActivity(activityDummyData1.user, { + await updateActivity(activityDummyData1.user, returnedActivities[0]._id, { ...activityDummyData1, - _id: returnedActivities[0]._id, itemName: "activitiesItem2", } as ActivitiesType); const returnedActivity2 = await getActivityById( @@ -61,9 +60,11 @@ describe("Activities controller tests", () => { expect(returnedActivity2?.itemName).to.equal("activitiesItem2"); await deleteActivity(activityDummyData1.user, returnedActivities[0]._id); - const returnedActivities3 = await getAllActivities(activityDummyData1.user); - expect(returnedActivities3.length).to.equal(0); + const returnedActivities3 = await getAllActivities(activityDummyData1.user); + expect(returnedActivities3.length).to.equal(0); - await expect(updateActivity(activityDummyData1.user, {} as ActivitiesType)).rejects.toThrowError("Missing"); + await expect( + updateActivity(activityDummyData1.user, "", {} as ActivitiesType), + ).rejects.toThrowError("Missing"); }); }); From 047593b86f58cfec0ab6dad908afe2cfb9145a85 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Tue, 27 Feb 2024 22:21:15 -0600 Subject: [PATCH 23/37] comments activities code --- src/controllers/activities.controller.ts | 4 ++++ src/routers/activities.router.ts | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/controllers/activities.controller.ts b/src/controllers/activities.controller.ts index 11d9c44..70af4ba 100644 --- a/src/controllers/activities.controller.ts +++ b/src/controllers/activities.controller.ts @@ -85,6 +85,10 @@ export const updateActivity = async ( ); } + if (await checkDuplicateItemName(activitiesFields.itemName)) { + throw new HttpError(HttpStatus.BAD_REQUEST, "Duplicate item name"); + } + const updatedActivity = await ActivitiesModel.findOneAndUpdate( { _id: activityId, user: user }, // Query to match the document by _id and user { $set: activitiesFields }, // Update operation diff --git a/src/routers/activities.router.ts b/src/routers/activities.router.ts index f11e3ac..74853c5 100644 --- a/src/routers/activities.router.ts +++ b/src/routers/activities.router.ts @@ -11,6 +11,7 @@ import { type ActivitiesType } from "../models/activities.model"; export const activitiesRouter = Router(); +//Add an activity //Note that the user field (which is part of activitiesType) in body is automatically populated by verifyToken middleware activitiesRouter.post( "/", @@ -30,6 +31,7 @@ activitiesRouter.post( }, ); +//Get all activities activitiesRouter.get( "/", async (req: Request, res: Response) => { @@ -48,6 +50,7 @@ activitiesRouter.get( }, ); +//Get a single activity by id activitiesRouter.get( "/:activityId", async (req: Request, res: Response) => { @@ -66,6 +69,7 @@ activitiesRouter.get( }, ); +//Update an activity activitiesRouter.put( "/:activityId", async (req: Request, res: Response) => { @@ -84,6 +88,7 @@ activitiesRouter.put( }, ); +//Delete an activity activitiesRouter.delete( "/:activityId", async (req: Request, res: Response) => { From 6db55aceb164cd04f8623ef5a29621a7c4d338fd Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Tue, 27 Feb 2024 22:21:57 -0600 Subject: [PATCH 24/37] implements part of skills --- src/controllers/skills.controller.ts | 22 +++++++++++++++++++++- src/routers/skills.router.ts | 4 ++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/controllers/skills.controller.ts b/src/controllers/skills.controller.ts index a8cd3be..fe897a5 100644 --- a/src/controllers/skills.controller.ts +++ b/src/controllers/skills.controller.ts @@ -2,7 +2,7 @@ import { SkillsModel, type SkillsType } from "../models/skills.model"; import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; import { checkDuplicateItemName } from "../utils/checkDuplicates"; -export const createSkills = async (skillsFields: SkillsType) => { +export const createSkill = async (skillsFields: SkillsType) => { try { if(await checkDuplicateItemName(skillsFields.itemName)){ throw new HttpError( @@ -28,3 +28,23 @@ export const createSkills = async (skillsFields: SkillsType) => { ); } }; + +export const getAllSkills = async (user: string) => { + try { + const skills = await SkillsModel.find({ user: user }); + return skills; + } 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, + "Skills retrieval failed", + { cause: err }, + ); + } +} \ No newline at end of file diff --git a/src/routers/skills.router.ts b/src/routers/skills.router.ts index 5c0edf2..04aa90e 100644 --- a/src/routers/skills.router.ts +++ b/src/routers/skills.router.ts @@ -1,5 +1,5 @@ import { Router, type Request, type Response } from "express"; -import { createSkills } from "../controllers/skills.controller"; +import { createSkill } from "../controllers/skills.controller"; import { HttpError, HttpStatus } from "../utils/errors"; import { type SkillsType } from "../models/skills.model"; @@ -10,7 +10,7 @@ skillsRouter.post( "/", async (req: Request, res: Response) => { try { - const skills = await createSkills(req.body); + const skills = await createSkill(req.body); res.status(HttpStatus.OK).json(skills); } catch (err: unknown) { if (err instanceof HttpError) { From 52513e3003b3562e4b12fcb8a518b50d70a47ddd Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Wed, 28 Feb 2024 11:14:40 -0600 Subject: [PATCH 25/37] implements education controller --- src/controllers/activities.controller.ts | 4 +- src/controllers/education.controller.ts | 138 +++++++++++++++++++++++ 2 files changed, 140 insertions(+), 2 deletions(-) diff --git a/src/controllers/activities.controller.ts b/src/controllers/activities.controller.ts index 70af4ba..da0e234 100644 --- a/src/controllers/activities.controller.ts +++ b/src/controllers/activities.controller.ts @@ -51,11 +51,11 @@ export const getAllActivities = async (user: string) => { export const getActivityById = async (user: string, activityId: string) => { try { - const activities = await ActivitiesModel.findOne({ + const activity = await ActivitiesModel.findOne({ user: user, _id: activityId, }); - return activities; + return activity; } catch (err: unknown) { //rethrow any errors as HttpErrors if (err instanceof HttpError) { diff --git a/src/controllers/education.controller.ts b/src/controllers/education.controller.ts index e69de29..143ca93 100644 --- a/src/controllers/education.controller.ts +++ b/src/controllers/education.controller.ts @@ -0,0 +1,138 @@ +import { EducationModel, type EducationType } from "../models/education.model"; +import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; +import { checkDuplicateItemName } from "../utils/checkDuplicates"; + +export const createEducation = async (educationFields: EducationType) => { + try { + if (await checkDuplicateItemName(educationFields.itemName)) { + throw new HttpError(HttpStatus.BAD_REQUEST, "Duplicate item name"); + } + + const newEducation = new EducationModel(educationFields); + await newEducation.save(); + return newEducation; + } catch (err: unknown) { + if (err instanceof HttpError) { + throw err; + } + + checkMongooseErrors(err); + + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "Education creation failed", + { cause: err }, + ); + } +}; + +export const getAllEducation = async (user: string) => { + try { + const education = await EducationModel.find({ user: user }); + return education; + } 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, + "Education retrieval failed", + { cause: err }, + ); + } +}; + +export const getActivityById = async (user: string, educationId: string) => { + try { + const education = await EducationModel.findOne({ + user: user, + _id: educationId, + }); + return education; + } 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, + "Education retrieval failed", + { cause: err }, + ); + } +}; + +export const updateEducation = async ( + user: string, + educationId: string, + educationFields: EducationType, +) => { + try { + if (!educationId) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Missing education ID for update", + ); + } + + if (await checkDuplicateItemName(educationFields.itemName)) { + throw new HttpError(HttpStatus.BAD_REQUEST, "Duplicate item name"); + } + + const updatedActivity = await EducationModel.findOneAndUpdate( + { _id: educationId, user: user }, // Query to match the document by _id and user + { $set: educationFields }, // Update operation + { new: true, runValidators: true }, // Options: return the updated document and run schema validators + ); + return updatedActivity; + } 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, + "Education update failed", + { cause: err }, + ); + } +}; + +export const deleteEducation = async (user: string, educationId: string) => { + try { + const deletedEducation = await EducationModel.findOneAndDelete({ + _id: educationId, + user: user, + }); + if (!deletedEducation) { + throw new HttpError( + HttpStatus.NOT_FOUND, + "Education not found or already deleted", + ); + } + return { message: "Education 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, + "Education deletion failed", + { cause: err }, + ); + } +}; From efdcdb3a5074c52e467a82efe0b556387344e02c Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Wed, 28 Feb 2024 11:54:06 -0600 Subject: [PATCH 26/37] implements education router --- src/routers/education.router.ts | 118 ++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/routers/education.router.ts diff --git a/src/routers/education.router.ts b/src/routers/education.router.ts new file mode 100644 index 0000000..a48aee3 --- /dev/null +++ b/src/routers/education.router.ts @@ -0,0 +1,118 @@ +import { Router, type Request, type Response } from "express"; +import { + createEducation, + getAllEducation, + getEducationById, + updateEducation, + deleteEducation, +} from "../controllers/education.controller"; +import { HttpError, HttpStatus } from "../utils/errors"; +import { type EducationType } from "../models/education.model"; + +export const educationRouter = Router(); + +//Add an education +//Note that the user field (which is part of EducationType) in body is automatically populated by verifyToken middleware +educationRouter.post( + "/", + async (req: Request, res: Response) => { + try { + const education = await createEducation(req.body); + res.status(HttpStatus.OK).json(education); + } 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 education +educationRouter.get( + "/", + async (req: Request, res: Response) => { + try { + const education = await getAllEducation(req.body.user); + res.status(HttpStatus.OK).json(education); + } 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 education by id +educationRouter.get( + "/:educationId", + async (req: Request, res: Response) => { + try { + const education = await getEducationById( + req.body.user, + req.params.educationId, + ); + res.status(HttpStatus.OK).json(education); + } 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 education +educationRouter.put( + "/:educationId", + async (req: Request, res: Response) => { + try { + const education = await updateEducation( + req.body.user, + req.params.educationId, + req.body, + ); + res.status(HttpStatus.OK).json(education); + } 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 education +educationRouter.delete( + "/:educationId", + async (req: Request, res: Response) => { + try { + const education = await deleteEducation( + req.body.user, + req.params.educationId, + ); + res.status(HttpStatus.OK).json(education); + } 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" }); + } + } + }, +); From b7d1fe32df1ea953fca21bca6d0c98d254a05314 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Wed, 28 Feb 2024 12:45:42 -0600 Subject: [PATCH 27/37] adds education tests --- src/controllers/education.controller.ts | 6 +- .../controllers.tests/activities.test.ts | 2 +- src/tests/controllers.tests/dummyData.ts | 10 +++ src/tests/controllers.tests/education.test.ts | 70 +++++++++++++++++++ 4 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 src/tests/controllers.tests/education.test.ts diff --git a/src/controllers/education.controller.ts b/src/controllers/education.controller.ts index 143ca93..8e98feb 100644 --- a/src/controllers/education.controller.ts +++ b/src/controllers/education.controller.ts @@ -46,7 +46,7 @@ export const getAllEducation = async (user: string) => { } }; -export const getActivityById = async (user: string, educationId: string) => { +export const getEducationById = async (user: string, educationId: string) => { try { const education = await EducationModel.findOne({ user: user, @@ -86,12 +86,12 @@ export const updateEducation = async ( throw new HttpError(HttpStatus.BAD_REQUEST, "Duplicate item name"); } - const updatedActivity = await EducationModel.findOneAndUpdate( + const updatedEducation = await EducationModel.findOneAndUpdate( { _id: educationId, user: user }, // Query to match the document by _id and user { $set: educationFields }, // Update operation { new: true, runValidators: true }, // Options: return the updated document and run schema validators ); - return updatedActivity; + return updatedEducation; } catch (err: unknown) { //rethrow any errors as HttpErrors if (err instanceof HttpError) { diff --git a/src/tests/controllers.tests/activities.test.ts b/src/tests/controllers.tests/activities.test.ts index fa6b2eb..ce15ff2 100644 --- a/src/tests/controllers.tests/activities.test.ts +++ b/src/tests/controllers.tests/activities.test.ts @@ -1,5 +1,5 @@ import { dbConnect, dbDisconnect } from "../dbHandler"; -import { ActivitiesType } from "../../models/activities.model"; +import { type ActivitiesType } from "../../models/activities.model"; import { activityDummyData1 } from "./dummyData"; import { createActivity, diff --git a/src/tests/controllers.tests/dummyData.ts b/src/tests/controllers.tests/dummyData.ts index ef8aff8..8ce1457 100644 --- a/src/tests/controllers.tests/dummyData.ts +++ b/src/tests/controllers.tests/dummyData.ts @@ -7,3 +7,13 @@ export const activityDummyData1 = { year: "year", location: "location", }; + +export const educationDummyData1 = { + user: "test", + itemName: "activitiesItem1", + bullets: ["example bullet"], + title: "title", + subtitle: "subtitle", + location: "location", + year: "year", +} diff --git a/src/tests/controllers.tests/education.test.ts b/src/tests/controllers.tests/education.test.ts new file mode 100644 index 0000000..0bef8b0 --- /dev/null +++ b/src/tests/controllers.tests/education.test.ts @@ -0,0 +1,70 @@ +import { dbConnect, dbDisconnect } from "../dbHandler"; +import { type EducationType } from "../../models/education.model"; +import { educationDummyData1 } from "./dummyData"; +import { + createEducation, + getAllEducation, + getEducationById, + updateEducation, + deleteEducation, +} from "../../controllers/education.controller"; +import { describe, test, expect, beforeEach, afterEach } from "vitest"; + +describe("Education controller tests", () => { + beforeEach(async () => dbConnect()); + afterEach(async () => dbDisconnect()); + + test("Adds and retrieves an education", async () => { + await createEducation(educationDummyData1 as EducationType); + const returnedEducation = await getAllEducation(educationDummyData1.user); + + //get back the 1 activity that was added + expect(returnedEducation.length).to.equal(1); + expect(returnedEducation[0]).toMatchObject(educationDummyData1); + + //Can't add duplicate name + await expect( + createEducation(educationDummyData1 as EducationType), + ).rejects.toThrowError(); + + const returnedEducation2 = await getAllEducation(educationDummyData1.user); + + //if duplicate, shouldn't add to db + expect(returnedEducation2.length).to.equal(1); + + const returnedEducation3 = await getAllEducation("fakeuserid"); + + //don't get records for a different user id + expect(returnedEducation3.length).to.equal(0); + }); + + test("Finds, updates, and deletes an education", async () => { + await createEducation(educationDummyData1 as EducationType); + const returnedEd = await getAllEducation(educationDummyData1.user); + + const returnedEducation = await getEducationById( + educationDummyData1.user, + returnedEd[0]._id, + ); + + expect(returnedEducation).toMatchObject(educationDummyData1); + + await updateEducation(educationDummyData1.user, returnedEd[0]._id, { + ...educationDummyData1, + itemName: "activitiesItem2", + } as EducationType); + const returnedEducation2 = await getEducationById( + educationDummyData1.user, + returnedEd[0]._id, + ); + expect(returnedEducation2?.itemName).to.equal("activitiesItem2"); + + await deleteEducation(educationDummyData1.user, returnedEd[0]._id); + const returnedEducation3 = await getAllEducation(educationDummyData1.user); + expect(returnedEducation3.length).to.equal(0); + + await expect( + updateEducation(educationDummyData1.user, "", {} as EducationType), + ).rejects.toThrowError("Missing"); + }); +}); From 3b306931689f246b168cf84832456116b71bd192 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Wed, 28 Feb 2024 12:47:41 -0600 Subject: [PATCH 28/37] updates tests --- src/tests/controllers.tests/activities.test.ts | 5 +++-- src/tests/controllers.tests/education.test.ts | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/tests/controllers.tests/activities.test.ts b/src/tests/controllers.tests/activities.test.ts index ce15ff2..9325f90 100644 --- a/src/tests/controllers.tests/activities.test.ts +++ b/src/tests/controllers.tests/activities.test.ts @@ -49,15 +49,16 @@ describe("Activities controller tests", () => { expect(returnedActivity).toMatchObject(activityDummyData1); + const newItem = "activitiesItem2"; await updateActivity(activityDummyData1.user, returnedActivities[0]._id, { ...activityDummyData1, - itemName: "activitiesItem2", + itemName: newItem, } as ActivitiesType); const returnedActivity2 = await getActivityById( activityDummyData1.user, returnedActivities[0]._id, ); - expect(returnedActivity2?.itemName).to.equal("activitiesItem2"); + expect(returnedActivity2?.itemName).to.equal(newItem); await deleteActivity(activityDummyData1.user, returnedActivities[0]._id); const returnedActivities3 = await getAllActivities(activityDummyData1.user); diff --git a/src/tests/controllers.tests/education.test.ts b/src/tests/controllers.tests/education.test.ts index 0bef8b0..0fc046d 100644 --- a/src/tests/controllers.tests/education.test.ts +++ b/src/tests/controllers.tests/education.test.ts @@ -18,7 +18,7 @@ describe("Education controller tests", () => { await createEducation(educationDummyData1 as EducationType); const returnedEducation = await getAllEducation(educationDummyData1.user); - //get back the 1 activity that was added + //get back the 1 education that was added expect(returnedEducation.length).to.equal(1); expect(returnedEducation[0]).toMatchObject(educationDummyData1); @@ -49,15 +49,16 @@ describe("Education controller tests", () => { expect(returnedEducation).toMatchObject(educationDummyData1); + const newItemName = "educationItem2"; await updateEducation(educationDummyData1.user, returnedEd[0]._id, { ...educationDummyData1, - itemName: "activitiesItem2", + itemName: newItemName, } as EducationType); const returnedEducation2 = await getEducationById( educationDummyData1.user, returnedEd[0]._id, ); - expect(returnedEducation2?.itemName).to.equal("activitiesItem2"); + expect(returnedEducation2?.itemName).to.equal(newItemName); await deleteEducation(educationDummyData1.user, returnedEd[0]._id); const returnedEducation3 = await getAllEducation(educationDummyData1.user); From 1e2adf294085e5ea04b8e728e1ce6eb545c43c5e Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Wed, 28 Feb 2024 12:56:21 -0600 Subject: [PATCH 29/37] adds education to root router --- src/routers/root.router.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routers/root.router.ts b/src/routers/root.router.ts index a5ef2c8..fc8942d 100644 --- a/src/routers/root.router.ts +++ b/src/routers/root.router.ts @@ -1,8 +1,10 @@ import { Router } from "express"; import { skillsRouter } from "./skills.router"; import { activitiesRouter } from "./activities.router"; +import { educationRouter } from "./education.router"; export const router = Router(); router.use("/skills", skillsRouter); router.use("/activities", activitiesRouter); +router.use("/education", educationRouter); From e6565b88ca7ffac779c490f466b310ff74b1cc22 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Wed, 28 Feb 2024 13:45:39 -0600 Subject: [PATCH 30/37] fixes checkDuplicates to work with update controllers --- src/controllers/activities.controller.ts | 2 +- src/controllers/education.controller.ts | 2 +- src/utils/checkDuplicates.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/controllers/activities.controller.ts b/src/controllers/activities.controller.ts index da0e234..87c185b 100644 --- a/src/controllers/activities.controller.ts +++ b/src/controllers/activities.controller.ts @@ -85,7 +85,7 @@ export const updateActivity = async ( ); } - if (await checkDuplicateItemName(activitiesFields.itemName)) { + if (await checkDuplicateItemName(activitiesFields.itemName, activityId)) { throw new HttpError(HttpStatus.BAD_REQUEST, "Duplicate item name"); } diff --git a/src/controllers/education.controller.ts b/src/controllers/education.controller.ts index 8e98feb..0b95898 100644 --- a/src/controllers/education.controller.ts +++ b/src/controllers/education.controller.ts @@ -82,7 +82,7 @@ export const updateEducation = async ( ); } - if (await checkDuplicateItemName(educationFields.itemName)) { + if (await checkDuplicateItemName(educationFields.itemName, educationId)) { throw new HttpError(HttpStatus.BAD_REQUEST, "Duplicate item name"); } diff --git a/src/utils/checkDuplicates.ts b/src/utils/checkDuplicates.ts index e713aec..57085b6 100644 --- a/src/utils/checkDuplicates.ts +++ b/src/utils/checkDuplicates.ts @@ -7,7 +7,7 @@ import { ProjectModel } from "../models/project.model"; import { SectionHeadingModel } from "../models/sectionHeading.model"; import { SkillsModel } from "../models/skills.model"; -export const checkDuplicateItemName = async (value: string): Promise => { +export const checkDuplicateItemName = async (value: string, excludedId: string | null = null): Promise => { const field = "itemName"; const models = [ ActivitiesModel, @@ -21,7 +21,7 @@ export const checkDuplicateItemName = async (value: string): Promise => // Check each model for the count of documents with the specified itemName value const checks = models.map((model) => - model.countDocuments({ [field]: value }).exec(), + model.countDocuments({ [field]: value, '_id': { $ne: excludedId } }).exec(), ); // Await all checks to resolve From 6def3d4578f727ccdf6416b29339b0f0c66f57fe Mon Sep 17 00:00:00 2001 From: seancrich11 Date: Thu, 29 Feb 2024 10:33:39 -0600 Subject: [PATCH 31/37] Merge From 838ebbb3c836484c81145aebdbff06aa320ffb44 Mon Sep 17 00:00:00 2001 From: seancrich11 Date: Thu, 29 Feb 2024 19:24:03 -0600 Subject: [PATCH 32/37] Added routers, controllers and tests for the rest of the resume items --- src/controllers/experience.controller.ts | 142 ++++++++++++++++++ src/controllers/heading.controller.ts | 142 ++++++++++++++++++ src/controllers/project.controller.ts | 142 ++++++++++++++++++ src/controllers/skills.controller.ts | 93 +++++++++++- src/models/experience.model.ts | 8 +- src/routers/experience.router.ts | 118 +++++++++++++++ src/routers/folder.router.ts | 0 src/routers/heading.router.ts | 118 +++++++++++++++ src/routers/project.router.ts | 118 +++++++++++++++ src/routers/resume.router.ts | 0 src/routers/skills.router.ts | 106 ++++++++++++- src/tests/controllers.tests/dummyData.ts | 39 +++++ .../controllers.tests/experience.test.ts | 71 +++++++++ src/tests/controllers.tests/folder.test.ts | 0 src/tests/controllers.tests/heading.test.ts | 71 +++++++++ src/tests/controllers.tests/project.test.ts | 71 +++++++++ src/tests/controllers.tests/resume.test.ts | 0 src/tests/controllers.tests/skills.test.ts | 71 +++++++++ 18 files changed, 1299 insertions(+), 11 deletions(-) create mode 100644 src/routers/experience.router.ts create mode 100644 src/routers/folder.router.ts create mode 100644 src/routers/heading.router.ts create mode 100644 src/routers/project.router.ts create mode 100644 src/routers/resume.router.ts create mode 100644 src/tests/controllers.tests/experience.test.ts create mode 100644 src/tests/controllers.tests/folder.test.ts create mode 100644 src/tests/controllers.tests/heading.test.ts create mode 100644 src/tests/controllers.tests/project.test.ts create mode 100644 src/tests/controllers.tests/resume.test.ts create mode 100644 src/tests/controllers.tests/skills.test.ts diff --git a/src/controllers/experience.controller.ts b/src/controllers/experience.controller.ts index e69de29..f180032 100644 --- a/src/controllers/experience.controller.ts +++ b/src/controllers/experience.controller.ts @@ -0,0 +1,142 @@ +import { + ExperienceModel, + type ExperienceType, + } from "../models/experience.model"; + import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; + import { checkDuplicateItemName } from "../utils/checkDuplicates"; + + export const createExperience = async (experienceFields: ExperienceType) => { + try { + if (await checkDuplicateItemName(experienceFields.itemName)) { + throw new HttpError(HttpStatus.BAD_REQUEST, "Duplicate item name"); + } + + const newExperience = new ExperienceModel(experienceFields); + await newExperience.save(); + return newExperience; + } catch (err: unknown) { + if (err instanceof HttpError) { + throw err; + } + + checkMongooseErrors(err); + + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "Experience creation failed", + { cause: err }, + ); + } + }; + + export const getAllExperiences = async (user: string) => { + try { + const experience = await ExperienceModel.find({ user: user }); + return experience; + } 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, + "Experience retrieval failed", + { cause: err }, + ); + } + }; + + export const getExperienceById = async (user: string, experienceId: string) => { + try { + const experience = await ExperienceModel.findOne({ + user: user, + _id: experienceId, + }); + return experience; + } 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, + "Experience retrieval failed", + { cause: err }, + ); + } + }; + + export const updateExperience = async ( + user: string, + experienceId: string, + experienceFields: ExperienceType, + ) => { + try { + if (!experienceId) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Missing experience ID for update", + ); + } + + if (await checkDuplicateItemName(experienceFields.itemName, experienceId)) { + throw new HttpError(HttpStatus.BAD_REQUEST, "Duplicate item name"); + } + + const updatedExperience = await ExperienceModel.findOneAndUpdate( + { _id: experienceId, user: user }, // Query to match the document by _id and user + { $set: experienceFields }, // Update operation + { new: true, runValidators: true }, // Options: return the updated document and run schema validators + ); + return updatedExperience; + } 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, + "Experience update failed", + { cause: err }, + ); + } + }; + + export const deleteExperience = async (user: string, experienceId: string) => { + try { + const deletedExperience = await ExperienceModel.findOneAndDelete({ + _id: experienceId, + user: user, + }); + if (!deletedExperience) { + throw new HttpError( + HttpStatus.NOT_FOUND, + "Experience not found or already deleted", + ); + } + return { message: "Experience 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, + "Experience deletion failed", + { cause: err }, + ); + } + }; + \ No newline at end of file diff --git a/src/controllers/heading.controller.ts b/src/controllers/heading.controller.ts index e69de29..a67b554 100644 --- a/src/controllers/heading.controller.ts +++ b/src/controllers/heading.controller.ts @@ -0,0 +1,142 @@ +import { + HeadingModel, + type HeadingType, + } from "../models/heading.model"; + import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; + import { checkDuplicateItemName } from "../utils/checkDuplicates"; + + export const createHeading = async (headingFields: HeadingType) => { + try { + if (await checkDuplicateItemName(headingFields.itemName)) { + throw new HttpError(HttpStatus.BAD_REQUEST, "Duplicate item name"); + } + + const newHeading = new HeadingModel(headingFields); + await newHeading.save(); + return newHeading; + } catch (err: unknown) { + if (err instanceof HttpError) { + throw err; + } + + checkMongooseErrors(err); + + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "Heading creation failed", + { cause: err }, + ); + } + }; + + export const getAllHeadings = async (user: string) => { + try { + const heading = await HeadingModel.find({ user: user }); + return heading; + } 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, + "Heading retrieval failed", + { cause: err }, + ); + } + }; + + export const getHeadingById = async (user: string, headingId: string) => { + try { + const heading = await HeadingModel.findOne({ + user: user, + _id: headingId, + }); + return heading; + } 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, + "Heading retrieval failed", + { cause: err }, + ); + } + }; + + export const updateHeading = async ( + user: string, + headingId: string, + headingFields: HeadingType, + ) => { + try { + if (!headingId) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Missing heading ID for update", + ); + } + + if (await checkDuplicateItemName(headingFields.itemName, headingId)) { + throw new HttpError(HttpStatus.BAD_REQUEST, "Duplicate item name"); + } + + const updatedHeading = await HeadingModel.findOneAndUpdate( + { _id: headingId, user: user }, // Query to match the document by _id and user + { $set: headingFields }, // Update operation + { new: true, runValidators: true }, // Options: return the updated document and run schema validators + ); + return updatedHeading; + } 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, + "Heading update failed", + { cause: err }, + ); + } + }; + + export const deleteHeading = async (user: string, headingId: string) => { + try { + const deletedHeading = await HeadingModel.findOneAndDelete({ + _id: headingId, + user: user, + }); + if (!deletedHeading) { + throw new HttpError( + HttpStatus.NOT_FOUND, + "Heading not found or already deleted", + ); + } + return { message: "Heading 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, + "Heading deletion failed", + { cause: err }, + ); + } + }; + \ No newline at end of file diff --git a/src/controllers/project.controller.ts b/src/controllers/project.controller.ts index e69de29..3757f0f 100644 --- a/src/controllers/project.controller.ts +++ b/src/controllers/project.controller.ts @@ -0,0 +1,142 @@ +import { + ProjectModel, + type ProjectType, + } from "../models/project.model"; + import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; + import { checkDuplicateItemName } from "../utils/checkDuplicates"; + + export const createProject = async (projectFields: ProjectType) => { + try { + if (await checkDuplicateItemName(projectFields.itemName)) { + throw new HttpError(HttpStatus.BAD_REQUEST, "Duplicate item name"); + } + + const newProject = new ProjectModel(projectFields); + await newProject.save(); + return newProject; + } catch (err: unknown) { + if (err instanceof HttpError) { + throw err; + } + + checkMongooseErrors(err); + + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "Project creation failed", + { cause: err }, + ); + } + }; + + export const getAllProjects = async (user: string) => { + try { + const project = await ProjectModel.find({ user: user }); + return project; + } 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, + "Project retrieval failed", + { cause: err }, + ); + } + }; + + export const getProjectById = async (user: string, projectId: string) => { + try { + const project = await ProjectModel.findOne({ + user: user, + _id: projectId, + }); + return project; + } 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, + "Project retrieval failed", + { cause: err }, + ); + } + }; + + export const updateProject = async ( + user: string, + projectId: string, + projectFields: ProjectType, + ) => { + try { + if (!projectId) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Missing project ID for update", + ); + } + + if (await checkDuplicateItemName(projectFields.itemName, projectId)) { + throw new HttpError(HttpStatus.BAD_REQUEST, "Duplicate item name"); + } + + const updatedProject = await ProjectModel.findOneAndUpdate( + { _id: projectId, user: user }, // Query to match the document by _id and user + { $set: projectFields }, // Update operation + { new: true, runValidators: true }, // Options: return the updated document and run schema validators + ); + return updatedProject; + } 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, + "Project update failed", + { cause: err }, + ); + } + }; + + export const deleteProject = async (user: string, projectId: string) => { + try { + const deletedProject = await ProjectModel.findOneAndDelete({ + _id: projectId, + user: user, + }); + if (!deletedProject) { + throw new HttpError( + HttpStatus.NOT_FOUND, + "Project not found or already deleted", + ); + } + return { message: "Project 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, + "Project deletion failed", + { cause: err }, + ); + } + }; + \ No newline at end of file diff --git a/src/controllers/skills.controller.ts b/src/controllers/skills.controller.ts index fe897a5..223ddbd 100644 --- a/src/controllers/skills.controller.ts +++ b/src/controllers/skills.controller.ts @@ -47,4 +47,95 @@ export const getAllSkills = async (user: string) => { { cause: err }, ); } -} \ No newline at end of file +} + +export const getSkillById = async (user: string, skillId: string) => { + try { + const skill = await SkillsModel.findOne({ + user: user, + _id: skillId, + }); + return skill; + } 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, + "Skill retrieval failed", + { cause: err }, + ); + } +}; + +export const updateSkill = async ( + user: string, + skillId: string, + skillsFields: SkillsType, +) => { + try { + if (!skillId) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Missing skill ID for update", + ); + } + + if (await checkDuplicateItemName(skillsFields.itemName, skillId)) { + throw new HttpError(HttpStatus.BAD_REQUEST, "Duplicate item name"); + } + + const updatedSkill = await SkillsModel.findOneAndUpdate( + { _id: skillId, user: user }, // Query to match the document by _id and user + { $set: skillsFields }, // Update operation + { new: true, runValidators: true }, // Options: return the updated document and run schema validators + ); + return updatedSkill; + } 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, + "Skill update failed", + { cause: err }, + ); + } +}; + +export const deleteSkill = async (user: string, skillId: string) => { + try { + const deletedSkill = await SkillsModel.findOneAndDelete({ + _id: skillId, + user: user, + }); + if (!deletedSkill) { + throw new HttpError( + HttpStatus.NOT_FOUND, + "Skill not found or already deleted", + ); + } + return { message: "Skill 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, + "Skill deletion failed", + { cause: err }, + ); + } +}; diff --git a/src/models/experience.model.ts b/src/models/experience.model.ts index 8fe7908..42184c2 100644 --- a/src/models/experience.model.ts +++ b/src/models/experience.model.ts @@ -5,24 +5,24 @@ import mongoose from "mongoose"; const Schema = mongoose.Schema; //typescript type corresponding with the mongoose schema structure -export interface experienceType extends mongoose.Document { +export interface ExperienceType extends mongoose.Document { user: string; itemName: string; bullets: string[]; title: string; subtitle: string; - date: string; + year: string; location: string; } //mongoose schema for an event document -const Experience = new Schema({ +const Experience = new Schema({ user: { type: String, required: true }, itemName: { type: String, required: true }, bullets: { type: [String], required: true }, title: { type: String, required: true }, subtitle: { type: String, required: true }, - date: { type: String, required: true }, + year: { type: String, required: true }, location: { type: String, required: true }, }); diff --git a/src/routers/experience.router.ts b/src/routers/experience.router.ts new file mode 100644 index 0000000..c72d450 --- /dev/null +++ b/src/routers/experience.router.ts @@ -0,0 +1,118 @@ +import { Router, type Request, type Response } from "express"; +import { + createExperience, + getAllExperiences, + getExperienceById, + updateExperience, + deleteExperience, +} from "../controllers/experience.controller"; +import { HttpError, HttpStatus } from "../utils/errors"; +import { type ExperienceType } from "../models/experience.model"; + +export const experienceRouter = Router(); + +//Add an experience +//Note that the user field (which is part of ExperienceType) in body is automatically populated by verifyToken middleware +experienceRouter.post( + "/", + async (req: Request, res: Response) => { + try { + const experience = await createExperience(req.body); + res.status(HttpStatus.OK).json(experience); + } 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 experience +experienceRouter.get( + "/", + async (req: Request, res: Response) => { + try { + const experience = await getAllExperiences(req.body.user); + res.status(HttpStatus.OK).json(experience); + } 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 experience by id +experienceRouter.get( + "/:experienceId", + async (req: Request, res: Response) => { + try { + const experience = await getExperienceById( + req.body.user, + req.params.experienceId, + ); + res.status(HttpStatus.OK).json(experience); + } 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 experience +experienceRouter.put( + "/:experienceId", + async (req: Request, res: Response) => { + try { + const experience = await updateExperience( + req.body.user, + req.params.experienceId, + req.body, + ); + res.status(HttpStatus.OK).json(experience); + } 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 experience +experienceRouter.delete( + "/:experienceId", + async (req: Request, res: Response) => { + try { + const experience = await deleteExperience( + req.body.user, + req.params.experienceId, + ); + res.status(HttpStatus.OK).json(experience); + } 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/routers/folder.router.ts b/src/routers/folder.router.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/routers/heading.router.ts b/src/routers/heading.router.ts new file mode 100644 index 0000000..e10fc26 --- /dev/null +++ b/src/routers/heading.router.ts @@ -0,0 +1,118 @@ +import { Router, type Request, type Response } from "express"; +import { + createHeading, + getAllHeadings, + getHeadingById, + updateHeading, + deleteHeading, +} from "../controllers/heading.controller"; +import { HttpError, HttpStatus } from "../utils/errors"; +import { type HeadingType } from "../models/heading.model"; + +export const headingRouter = Router(); + +//Add an heading +//Note that the user field (which is part of HeadingType) in body is automatically populated by verifyToken middleware +headingRouter.post( + "/", + async (req: Request, res: Response) => { + try { + const heading = await createHeading(req.body); + res.status(HttpStatus.OK).json(heading); + } 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 heading +headingRouter.get( + "/", + async (req: Request, res: Response) => { + try { + const heading = await getAllHeadings(req.body.user); + res.status(HttpStatus.OK).json(heading); + } 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 heading by id +headingRouter.get( + "/:headingId", + async (req: Request, res: Response) => { + try { + const heading = await getHeadingById( + req.body.user, + req.params.headingId, + ); + res.status(HttpStatus.OK).json(heading); + } 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 heading +headingRouter.put( + "/:headingId", + async (req: Request, res: Response) => { + try { + const heading = await updateHeading( + req.body.user, + req.params.headingId, + req.body, + ); + res.status(HttpStatus.OK).json(heading); + } 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 heading +headingRouter.delete( + "/:headingId", + async (req: Request, res: Response) => { + try { + const heading = await deleteHeading( + req.body.user, + req.params.headingId, + ); + res.status(HttpStatus.OK).json(heading); + } 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/routers/project.router.ts b/src/routers/project.router.ts new file mode 100644 index 0000000..b4c1fc4 --- /dev/null +++ b/src/routers/project.router.ts @@ -0,0 +1,118 @@ +import { Router, type Request, type Response } from "express"; +import { + createProject, + getAllProjects, + getProjectById, + updateProject, + deleteProject, +} from "../controllers/project.controller"; +import { HttpError, HttpStatus } from "../utils/errors"; +import { type ProjectType } from "../models/project.model"; + +export const projectRouter = Router(); + +//Add an project +//Note that the user field (which is part of ProjectType) in body is automatically populated by verifyToken middleware +projectRouter.post( + "/", + async (req: Request, res: Response) => { + try { + const project = await createProject(req.body); + res.status(HttpStatus.OK).json(project); + } 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 project +projectRouter.get( + "/", + async (req: Request, res: Response) => { + try { + const project = await getAllProjects(req.body.user); + res.status(HttpStatus.OK).json(project); + } 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 project by id +projectRouter.get( + "/:projectId", + async (req: Request, res: Response) => { + try { + const project = await getProjectById( + req.body.user, + req.params.projectId, + ); + res.status(HttpStatus.OK).json(project); + } 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 project +projectRouter.put( + "/:projectId", + async (req: Request, res: Response) => { + try { + const project = await updateProject( + req.body.user, + req.params.projectId, + req.body, + ); + res.status(HttpStatus.OK).json(project); + } 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 project +projectRouter.delete( + "/:projectId", + async (req: Request, res: Response) => { + try { + const project = await deleteProject( + req.body.user, + req.params.projectId, + ); + res.status(HttpStatus.OK).json(project); + } 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/routers/resume.router.ts b/src/routers/resume.router.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/routers/skills.router.ts b/src/routers/skills.router.ts index 04aa90e..b96df0e 100644 --- a/src/routers/skills.router.ts +++ b/src/routers/skills.router.ts @@ -1,17 +1,24 @@ import { Router, type Request, type Response } from "express"; -import { createSkill } from "../controllers/skills.controller"; +import { + createSkill, + getAllSkills, + getSkillById, + updateSkill, + deleteSkill, +} from "../controllers/skills.controller"; import { HttpError, HttpStatus } from "../utils/errors"; import { type SkillsType } from "../models/skills.model"; -export const skillsRouter = Router(); +export const skillRouter = Router(); -//Note that the user field (which is part of skillsType) in body is automatically populated by verifyToken middleware -skillsRouter.post( +//Add an skill +//Note that the user field (which is part of SkillsType) in body is automatically populated by verifyToken middleware +skillRouter.post( "/", async (req: Request, res: Response) => { try { - const skills = await createSkill(req.body); - res.status(HttpStatus.OK).json(skills); + const skill = await createSkill(req.body); + res.status(HttpStatus.OK).json(skill); } catch (err: unknown) { if (err instanceof HttpError) { res.status(err.errorCode).json({ error: err.message }); @@ -23,3 +30,90 @@ skillsRouter.post( } }, ); + +//Get all skill +skillRouter.get( + "/", + async (req: Request, res: Response) => { + try { + const skill = await getAllSkills(req.body.user); + res.status(HttpStatus.OK).json(skill); + } 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 skill by id +skillRouter.get( + "/:skillId", + async (req: Request, res: Response) => { + try { + const skill = await getSkillById( + req.body.user, + req.params.skillId, + ); + res.status(HttpStatus.OK).json(skill); + } 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 skill +skillRouter.put( + "/:skillId", + async (req: Request, res: Response) => { + try { + const skill = await updateSkill( + req.body.user, + req.params.skillId, + req.body, + ); + res.status(HttpStatus.OK).json(skill); + } 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 skill +skillRouter.delete( + "/:skillId", + async (req: Request, res: Response) => { + try { + const skill = await deleteSkill( + req.body.user, + req.params.skillId, + ); + res.status(HttpStatus.OK).json(skill); + } 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 8ce1457..ed8b27e 100644 --- a/src/tests/controllers.tests/dummyData.ts +++ b/src/tests/controllers.tests/dummyData.ts @@ -17,3 +17,42 @@ export const educationDummyData1 = { location: "location", year: "year", } + +export const experienceDummyData1 = { + user: "test", + itemName: "activitiesItem1", + bullets: ["example bullet"], + title: "title", + subtitle: "subtitle", + location: "location", + year: "year", +} + +export const headingItemDummy = { + item: "item", + href: null, +} + +export const headingDummyData1 = { + user: "user", + itemName: "itemName", + name: "name", + items: [headingItemDummy], +} + +export const projectDummyData1 = { + user: "test", + itemName: "activitiesItem1", + bullets: ["example bullet"], + title: "title", + year: "year", +} + +export const skillsDummyData1 = { + user: "test", + itemName: "activitiesItem1", + title: "title", + description: "description" +} + + diff --git a/src/tests/controllers.tests/experience.test.ts b/src/tests/controllers.tests/experience.test.ts new file mode 100644 index 0000000..435bcfb --- /dev/null +++ b/src/tests/controllers.tests/experience.test.ts @@ -0,0 +1,71 @@ +import { dbConnect, dbDisconnect } from "../dbHandler"; +import { type ExperienceType } from "../../models/experience.model"; +import { experienceDummyData1 } from "./dummyData"; +import { + createExperience, + getAllExperiences, + getExperienceById, + updateExperience, + deleteExperience, +} from "../../controllers/experience.controller"; +import { describe, test, expect, beforeEach, afterEach } from "vitest"; + +describe("Experience controller tests", () => { + beforeEach(async () => dbConnect()); + afterEach(async () => dbDisconnect()); + + test("Adds and retrieves an experience", async () => { + await createExperience(experienceDummyData1 as ExperienceType); + const returnedExperience = await getAllExperiences(experienceDummyData1.user); + + //get back the 1 experience that was added + expect(returnedExperience.length).to.equal(1); + expect(returnedExperience[0]).toMatchObject(experienceDummyData1); + + //Can't add duplicate name + await expect( + createExperience(experienceDummyData1 as ExperienceType), + ).rejects.toThrowError(); + + const returnedExperience2 = await getAllExperiences(experienceDummyData1.user); + + //if duplicate, shouldn't add to db + expect(returnedExperience2.length).to.equal(1); + + const returnedExperience3 = await getAllExperiences("fakeuserid"); + + //don't get records for a different user id + expect(returnedExperience3.length).to.equal(0); + }); + + test("Finds, updates, and deletes an experience", async () => { + await createExperience(experienceDummyData1 as ExperienceType); + const returnedEd = await getAllExperiences(experienceDummyData1.user); + + const returnedExperience = await getExperienceById( + experienceDummyData1.user, + returnedEd[0]._id, + ); + + expect(returnedExperience).toMatchObject(experienceDummyData1); + + const newItemName = "experienceItem2"; + await updateExperience(experienceDummyData1.user, returnedEd[0]._id, { + ...experienceDummyData1, + itemName: newItemName, + } as ExperienceType); + const returnedExperience2 = await getExperienceById( + experienceDummyData1.user, + returnedEd[0]._id, + ); + expect(returnedExperience2?.itemName).to.equal(newItemName); + + await deleteExperience(experienceDummyData1.user, returnedEd[0]._id); + const returnedExperience3 = await getAllExperiences(experienceDummyData1.user); + expect(returnedExperience3.length).to.equal(0); + + await expect( + updateExperience(experienceDummyData1.user, "", {} as ExperienceType), + ).rejects.toThrowError("Missing"); + }); +}); diff --git a/src/tests/controllers.tests/folder.test.ts b/src/tests/controllers.tests/folder.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/controllers.tests/heading.test.ts b/src/tests/controllers.tests/heading.test.ts new file mode 100644 index 0000000..93f1253 --- /dev/null +++ b/src/tests/controllers.tests/heading.test.ts @@ -0,0 +1,71 @@ +import { dbConnect, dbDisconnect } from "../dbHandler"; +import { type HeadingType } from "../../models/heading.model"; +import { headingDummyData1 } from "./dummyData"; +import { + createHeading, + getAllHeadings, + getHeadingById, + updateHeading, + deleteHeading, +} from "../../controllers/heading.controller"; +import { describe, test, expect, beforeEach, afterEach } from "vitest"; + +describe("Heading controller tests", () => { + beforeEach(async () => dbConnect()); + afterEach(async () => dbDisconnect()); + + test("Adds and retrieves an heading", async () => { + await createHeading(headingDummyData1 as HeadingType); + const returnedHeading = await getAllHeadings(headingDummyData1.user); + + //get back the 1 heading that was added + expect(returnedHeading.length).to.equal(1); + expect(returnedHeading[0]).toMatchObject(headingDummyData1); + + //Can't add duplicate name + await expect( + createHeading(headingDummyData1 as HeadingType), + ).rejects.toThrowError(); + + const returnedHeading2 = await getAllHeadings(headingDummyData1.user); + + //if duplicate, shouldn't add to db + expect(returnedHeading2.length).to.equal(1); + + const returnedHeading3 = await getAllHeadings("fakeuserid"); + + //don't get records for a different user id + expect(returnedHeading3.length).to.equal(0); + }); + + test("Finds, updates, and deletes an heading", async () => { + await createHeading(headingDummyData1 as HeadingType); + const returnedEd = await getAllHeadings(headingDummyData1.user); + + const returnedHeading = await getHeadingById( + headingDummyData1.user, + returnedEd[0]._id, + ); + + expect(returnedHeading).toMatchObject(headingDummyData1); + + const newItemName = "headingItem2"; + await updateHeading(headingDummyData1.user, returnedEd[0]._id, { + ...headingDummyData1, + itemName: newItemName, + } as HeadingType); + const returnedHeading2 = await getHeadingById( + headingDummyData1.user, + returnedEd[0]._id, + ); + expect(returnedHeading2?.itemName).to.equal(newItemName); + + await deleteHeading(headingDummyData1.user, returnedEd[0]._id); + const returnedHeading3 = await getAllHeadings(headingDummyData1.user); + expect(returnedHeading3.length).to.equal(0); + + await expect( + updateHeading(headingDummyData1.user, "", {} as HeadingType), + ).rejects.toThrowError("Missing"); + }); +}); diff --git a/src/tests/controllers.tests/project.test.ts b/src/tests/controllers.tests/project.test.ts new file mode 100644 index 0000000..76aeb15 --- /dev/null +++ b/src/tests/controllers.tests/project.test.ts @@ -0,0 +1,71 @@ +import { dbConnect, dbDisconnect } from "../dbHandler"; +import { type ProjectType } from "../../models/project.model"; +import { projectDummyData1 } from "./dummyData"; +import { + createProject, + getAllProjects, + getProjectById, + updateProject, + deleteProject, +} from "../../controllers/project.controller"; +import { describe, test, expect, beforeEach, afterEach } from "vitest"; + +describe("Project controller tests", () => { + beforeEach(async () => dbConnect()); + afterEach(async () => dbDisconnect()); + + test("Adds and retrieves an project", async () => { + await createProject(projectDummyData1 as ProjectType); + const returnedProject = await getAllProjects(projectDummyData1.user); + + //get back the 1 project that was added + expect(returnedProject.length).to.equal(1); + expect(returnedProject[0]).toMatchObject(projectDummyData1); + + //Can't add duplicate name + await expect( + createProject(projectDummyData1 as ProjectType), + ).rejects.toThrowError(); + + const returnedProject2 = await getAllProjects(projectDummyData1.user); + + //if duplicate, shouldn't add to db + expect(returnedProject2.length).to.equal(1); + + const returnedProject3 = await getAllProjects("fakeuserid"); + + //don't get records for a different user id + expect(returnedProject3.length).to.equal(0); + }); + + test("Finds, updates, and deletes an project", async () => { + await createProject(projectDummyData1 as ProjectType); + const returnedEd = await getAllProjects(projectDummyData1.user); + + const returnedProject = await getProjectById( + projectDummyData1.user, + returnedEd[0]._id, + ); + + expect(returnedProject).toMatchObject(projectDummyData1); + + const newItemName = "projectItem2"; + await updateProject(projectDummyData1.user, returnedEd[0]._id, { + ...projectDummyData1, + itemName: newItemName, + } as ProjectType); + const returnedProject2 = await getProjectById( + projectDummyData1.user, + returnedEd[0]._id, + ); + expect(returnedProject2?.itemName).to.equal(newItemName); + + await deleteProject(projectDummyData1.user, returnedEd[0]._id); + const returnedProject3 = await getAllProjects(projectDummyData1.user); + expect(returnedProject3.length).to.equal(0); + + await expect( + updateProject(projectDummyData1.user, "", {} as ProjectType), + ).rejects.toThrowError("Missing"); + }); +}); diff --git a/src/tests/controllers.tests/resume.test.ts b/src/tests/controllers.tests/resume.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/controllers.tests/skills.test.ts b/src/tests/controllers.tests/skills.test.ts new file mode 100644 index 0000000..f550de9 --- /dev/null +++ b/src/tests/controllers.tests/skills.test.ts @@ -0,0 +1,71 @@ +import { dbConnect, dbDisconnect } from "../dbHandler"; +import { type SkillsType } from "../../models/skills.model"; +import { skillsDummyData1 } from "./dummyData"; +import { + createSkill, + getAllSkills, + getSkillById, + updateSkill, + deleteSkill, +} from "../../controllers/skills.controller"; +import { describe, test, expect, beforeEach, afterEach } from "vitest"; + +describe("Skills controller tests", () => { + beforeEach(async () => dbConnect()); + afterEach(async () => dbDisconnect()); + + test("Adds and retrieves an skills", async () => { + await createSkill(skillsDummyData1 as SkillsType); + const returnedSkills = await getAllSkills(skillsDummyData1.user); + + //get back the 1 skills that was added + expect(returnedSkills.length).to.equal(1); + expect(returnedSkills[0]).toMatchObject(skillsDummyData1); + + //Can't add duplicate name + await expect( + createSkill(skillsDummyData1 as SkillsType), + ).rejects.toThrowError(); + + const returnedSkills2 = await getAllSkills(skillsDummyData1.user); + + //if duplicate, shouldn't add to db + expect(returnedSkills2.length).to.equal(1); + + const returnedSkills3 = await getAllSkills("fakeuserid"); + + //don't get records for a different user id + expect(returnedSkills3.length).to.equal(0); + }); + + test("Finds, updates, and deletes an skills", async () => { + await createSkill(skillsDummyData1 as SkillsType); + const returnedEd = await getAllSkills(skillsDummyData1.user); + + const returnedSkills = await getSkillById( + skillsDummyData1.user, + returnedEd[0]._id, + ); + + expect(returnedSkills).toMatchObject(skillsDummyData1); + + const newItemName = "skillsItem2"; + await updateSkill(skillsDummyData1.user, returnedEd[0]._id, { + ...skillsDummyData1, + itemName: newItemName, + } as SkillsType); + const returnedSkills2 = await getSkillById( + skillsDummyData1.user, + returnedEd[0]._id, + ); + expect(returnedSkills2?.itemName).to.equal(newItemName); + + await deleteSkill(skillsDummyData1.user, returnedEd[0]._id); + const returnedSkills3 = await getAllSkills(skillsDummyData1.user); + expect(returnedSkills3.length).to.equal(0); + + await expect( + updateSkill(skillsDummyData1.user, "", {} as SkillsType), + ).rejects.toThrowError("Missing"); + }); +}); From 293275d371a327067f67c0b70d38327f569282bb Mon Sep 17 00:00:00 2001 From: seancrich11 Date: Thu, 29 Feb 2024 19:26:54 -0600 Subject: [PATCH 33/37] Fix root.router --- src/routers/root.router.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routers/root.router.ts b/src/routers/root.router.ts index fc8942d..1e41d83 100644 --- a/src/routers/root.router.ts +++ b/src/routers/root.router.ts @@ -1,10 +1,10 @@ import { Router } from "express"; -import { skillsRouter } from "./skills.router"; +import { skillRouter } from "./skills.router"; import { activitiesRouter } from "./activities.router"; import { educationRouter } from "./education.router"; export const router = Router(); -router.use("/skills", skillsRouter); +router.use("/skills", skillRouter); router.use("/activities", activitiesRouter); router.use("/education", educationRouter); From bc25cf1bd0cf70af6c02ac14348436a2d1d2bf78 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Sat, 2 Mar 2024 17:51:14 -0600 Subject: [PATCH 34/37] adjusts project model --- src/models/project.model.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/models/project.model.ts b/src/models/project.model.ts index 784d78f..1e52305 100644 --- a/src/models/project.model.ts +++ b/src/models/project.model.ts @@ -8,6 +8,7 @@ export interface ProjectType extends mongoose.Document { itemName: string; bullets: string[]; title: string; + technologies?: string; year: string; } @@ -17,6 +18,7 @@ const Project = new Schema({ itemName: { type: String, required: true }, bullets: { type: [String], required: true }, title: { type: String, required: true }, + technologies: {type: String, required: false}, year: { type: String, required: true }, }); From b73a3486f58cd2dbf1ae68ec571c7f8b2e81d4f0 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Sun, 3 Mar 2024 16:54:21 -0600 Subject: [PATCH 35/37] implements resume controller, router, test, and correct itemId list updating functionality --- src/controllers/activities.controller.ts | 7 + src/controllers/education.controller.ts | 7 + src/controllers/experience.controller.ts | 7 + src/controllers/heading.controller.ts | 7 + src/controllers/project.controller.ts | 7 + src/controllers/resume.controller.ts | 141 +++++++++++++++++++++ src/controllers/skills.controller.ts | 7 + src/models/resume.model.ts | 6 +- src/routers/resume.router.ts | 119 +++++++++++++++++ src/routers/root.router.ts | 8 ++ src/tests/controllers.tests/dummyData.ts | 9 ++ src/tests/controllers.tests/folder.test.ts | 0 src/tests/controllers.tests/resume.test.ts | 88 +++++++++++++ 13 files changed, 410 insertions(+), 3 deletions(-) delete mode 100644 src/tests/controllers.tests/folder.test.ts diff --git a/src/controllers/activities.controller.ts b/src/controllers/activities.controller.ts index 87c185b..8423bbd 100644 --- a/src/controllers/activities.controller.ts +++ b/src/controllers/activities.controller.ts @@ -2,6 +2,8 @@ import { ActivitiesModel, type ActivitiesType, } from "../models/activities.model"; +import { ResumeModel } from "../models/resume.model"; +import mongoose from "mongoose"; import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; import { checkDuplicateItemName } from "../utils/checkDuplicates"; @@ -113,6 +115,11 @@ export const updateActivity = async ( export const deleteActivity = async (user: string, activityId: string) => { try { + await ResumeModel.updateMany( + { itemIds: new mongoose.Types.ObjectId(activityId) }, + { $pull: { itemIds: new mongoose.Types.ObjectId(activityId) } } + ); + const deletedActivity = await ActivitiesModel.findOneAndDelete({ _id: activityId, user: user, diff --git a/src/controllers/education.controller.ts b/src/controllers/education.controller.ts index 0b95898..93653f5 100644 --- a/src/controllers/education.controller.ts +++ b/src/controllers/education.controller.ts @@ -1,4 +1,6 @@ import { EducationModel, type EducationType } from "../models/education.model"; +import { ResumeModel } from "../models/resume.model"; +import mongoose from "mongoose"; import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; import { checkDuplicateItemName } from "../utils/checkDuplicates"; @@ -110,6 +112,11 @@ export const updateEducation = async ( export const deleteEducation = async (user: string, educationId: string) => { try { + await ResumeModel.updateMany( + { itemIds: new mongoose.Types.ObjectId(educationId) }, + { $pull: { itemIds: new mongoose.Types.ObjectId(educationId) } } + ); + const deletedEducation = await EducationModel.findOneAndDelete({ _id: educationId, user: user, diff --git a/src/controllers/experience.controller.ts b/src/controllers/experience.controller.ts index f180032..dd9c8bb 100644 --- a/src/controllers/experience.controller.ts +++ b/src/controllers/experience.controller.ts @@ -2,6 +2,8 @@ import { ExperienceModel, type ExperienceType, } from "../models/experience.model"; + import { ResumeModel } from "../models/resume.model"; +import mongoose from "mongoose"; import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; import { checkDuplicateItemName } from "../utils/checkDuplicates"; @@ -113,6 +115,11 @@ import { export const deleteExperience = async (user: string, experienceId: string) => { try { + await ResumeModel.updateMany( + { itemIds: new mongoose.Types.ObjectId(experienceId) }, + { $pull: { itemIds: new mongoose.Types.ObjectId(experienceId) } } + ); + const deletedExperience = await ExperienceModel.findOneAndDelete({ _id: experienceId, user: user, diff --git a/src/controllers/heading.controller.ts b/src/controllers/heading.controller.ts index a67b554..da5d4ce 100644 --- a/src/controllers/heading.controller.ts +++ b/src/controllers/heading.controller.ts @@ -2,6 +2,8 @@ import { HeadingModel, type HeadingType, } from "../models/heading.model"; + import { ResumeModel } from "../models/resume.model"; +import mongoose from "mongoose"; import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; import { checkDuplicateItemName } from "../utils/checkDuplicates"; @@ -113,6 +115,11 @@ import { export const deleteHeading = async (user: string, headingId: string) => { try { + await ResumeModel.updateMany( + { itemIds: new mongoose.Types.ObjectId(headingId) }, + { $pull: { itemIds: new mongoose.Types.ObjectId(headingId) } } + ); + const deletedHeading = await HeadingModel.findOneAndDelete({ _id: headingId, user: user, diff --git a/src/controllers/project.controller.ts b/src/controllers/project.controller.ts index 3757f0f..de67f97 100644 --- a/src/controllers/project.controller.ts +++ b/src/controllers/project.controller.ts @@ -2,6 +2,8 @@ import { ProjectModel, type ProjectType, } from "../models/project.model"; + import { ResumeModel } from "../models/resume.model"; +import mongoose from "mongoose"; import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; import { checkDuplicateItemName } from "../utils/checkDuplicates"; @@ -113,6 +115,11 @@ import { export const deleteProject = async (user: string, projectId: string) => { try { + await ResumeModel.updateMany( + { itemIds: new mongoose.Types.ObjectId(projectId) }, + { $pull: { itemIds: new mongoose.Types.ObjectId(projectId) } } + ); + const deletedProject = await ProjectModel.findOneAndDelete({ _id: projectId, user: user, diff --git a/src/controllers/resume.controller.ts b/src/controllers/resume.controller.ts index e69de29..6ee7f04 100644 --- a/src/controllers/resume.controller.ts +++ b/src/controllers/resume.controller.ts @@ -0,0 +1,141 @@ +import { ResumeModel, type resumeType } from "../models/resume.model"; +import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; +import { checkDuplicateItemName } from "../utils/checkDuplicates"; + +export const createResume = async (resumesFields: resumeType) => { + try { + if(await checkDuplicateItemName(resumesFields.itemName)){ + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Duplicate item name", + ) + } + + const newResumes = new ResumeModel(resumesFields); + await newResumes.save(); + return newResumes; + } catch (err: unknown) { + if (err instanceof HttpError) { + throw err; + } + + checkMongooseErrors(err); + + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "Resume creation failed", + { cause: err }, + ); + } +}; + +export const getAllResumes = async (user: string) => { + try { + const resumes = await ResumeModel.find({ user: user }); + return resumes; + } 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, + "Resumes retrieval failed", + { cause: err }, + ); + } +} + +export const getResumeById = async (user: string, resumeId: string) => { + try { + const resume = await ResumeModel.findOne({ + user: user, + _id: resumeId, + }); + return resume; + } 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, + "Resume retrieval failed", + { cause: err }, + ); + } +}; + +export const updateResume = async ( + user: string, + resumeId: string, + resumesFields: resumeType, +) => { + try { + if (!resumeId) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Missing resume ID for update", + ); + } + + if (await checkDuplicateItemName(resumesFields.itemName, resumeId)) { + throw new HttpError(HttpStatus.BAD_REQUEST, "Duplicate item name"); + } + + const updatedResume = await ResumeModel.findOneAndUpdate( + { _id: resumeId, user: user }, // Query to match the document by _id and user + { $set: resumesFields }, // Update operation + { new: true, runValidators: true }, // Options: return the updated document and run schema validators + ); + return updatedResume; + } 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, + "Resume update failed", + { cause: err }, + ); + } +}; + +export const deleteResume = async (user: string, resumeId: string) => { + try { + const deletedResume = await ResumeModel.findOneAndDelete({ + _id: resumeId, + user: user, + }); + if (!deletedResume) { + throw new HttpError( + HttpStatus.NOT_FOUND, + "Resume not found or already deleted", + ); + } + return { message: "Resume 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, + "Resume deletion failed", + { cause: err }, + ); + } +}; diff --git a/src/controllers/skills.controller.ts b/src/controllers/skills.controller.ts index 223ddbd..9a80cd5 100644 --- a/src/controllers/skills.controller.ts +++ b/src/controllers/skills.controller.ts @@ -1,4 +1,6 @@ import { SkillsModel, type SkillsType } from "../models/skills.model"; +import { ResumeModel } from "../models/resume.model"; +import mongoose from "mongoose"; import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; import { checkDuplicateItemName } from "../utils/checkDuplicates"; @@ -113,6 +115,11 @@ export const updateSkill = async ( export const deleteSkill = async (user: string, skillId: string) => { try { + await ResumeModel.updateMany( + { itemIds: new mongoose.Types.ObjectId(skillId) }, + { $pull: { itemIds: new mongoose.Types.ObjectId(skillId) } } + ); + const deletedSkill = await SkillsModel.findOneAndDelete({ _id: skillId, user: user, diff --git a/src/models/resume.model.ts b/src/models/resume.model.ts index 2f2682b..6e9e1df 100644 --- a/src/models/resume.model.ts +++ b/src/models/resume.model.ts @@ -7,13 +7,13 @@ const Schema = mongoose.Schema; export interface resumeType extends mongoose.Document { user: string; itemName: string; - itemIds: mongoose.Schema.Types.ObjectId[]; - templateId: mongoose.Schema.Types.ObjectId; + itemIds: mongoose.Types.ObjectId[]; + templateId: mongoose.Types.ObjectId; } const Resume = new Schema({ user: { type: String, required: true }, - itemName: { type: String, required: true }, + itemName: { type: String, required: true, unique: true }, itemIds: { type: [Schema.Types.ObjectId], required: true }, templateId: { type: Schema.Types.ObjectId, required: true }, }); diff --git a/src/routers/resume.router.ts b/src/routers/resume.router.ts index e69de29..23eabef 100644 --- a/src/routers/resume.router.ts +++ b/src/routers/resume.router.ts @@ -0,0 +1,119 @@ +import { Router, type Request, type Response } from "express"; +import { + createResume, + getAllResumes, + getResumeById, + updateResume, + deleteResume, +} from "../controllers/resume.controller"; +import { HttpError, HttpStatus } from "../utils/errors"; +import { type resumeType } from "../models/resume.model"; + +export const resumeRouter = Router(); + +//Add an resume +//Note that the user field (which is part of resumesType) in body is automatically populated by verifyToken middleware +resumeRouter.post( + "/", + async (req: Request, res: Response) => { + try { + const resume = await createResume(req.body); + res.status(HttpStatus.OK).json(resume); + } 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 resumes +resumeRouter.get( + "/", + async (req: Request, res: Response) => { + try { + const resume = await getAllResumes(req.body.user); + res.status(HttpStatus.OK).json(resume); + } 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 resume by id +resumeRouter.get( + "/:resumeId", + async (req: Request, res: Response) => { + try { + const resume = await getResumeById( + req.body.user, + req.params.resumeId, + ); + res.status(HttpStatus.OK).json(resume); + } 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 resume +resumeRouter.put( + "/:resumeId", + async (req: Request, res: Response) => { + try { + const resume = await updateResume( + req.body.user, + req.params.resumeId, + req.body, + ); + res.status(HttpStatus.OK).json(resume); + } 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 resume +resumeRouter.delete( + "/:resumeId", + async (req: Request, res: Response) => { + try { + const resume = await deleteResume( + req.body.user, + req.params.resumeId, + ); + res.status(HttpStatus.OK).json(resume); + } 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/routers/root.router.ts b/src/routers/root.router.ts index 1e41d83..81b4e19 100644 --- a/src/routers/root.router.ts +++ b/src/routers/root.router.ts @@ -2,9 +2,17 @@ import { Router } from "express"; import { skillRouter } from "./skills.router"; import { activitiesRouter } from "./activities.router"; import { educationRouter } from "./education.router"; +import { experienceRouter } from "./experience.router"; +import { headingRouter } from "./heading.router"; +import { projectRouter } from "./project.router"; +import { resumeRouter } from "./resume.router"; export const router = Router(); router.use("/skills", skillRouter); router.use("/activities", activitiesRouter); router.use("/education", educationRouter); +router.use("/experience", experienceRouter); +router.use("/headings", headingRouter); +router.use("/projects", projectRouter); +router.use("/resumes", resumeRouter); diff --git a/src/tests/controllers.tests/dummyData.ts b/src/tests/controllers.tests/dummyData.ts index ed8b27e..794a545 100644 --- a/src/tests/controllers.tests/dummyData.ts +++ b/src/tests/controllers.tests/dummyData.ts @@ -1,3 +1,5 @@ +import mongoose from "mongoose"; + export const activityDummyData1 = { user: "test", itemName: "activitesItem1", @@ -55,4 +57,11 @@ export const skillsDummyData1 = { description: "description" } +export const resumeDummyData1 = { + user: "test", + itemName: "resumeItem1", + itemIds: [new mongoose.Types.ObjectId("65e4f54db1e12e776e01cf31")], + templateId: new mongoose.Types.ObjectId("75e4f54db1e12e776e01cf31"), +} + diff --git a/src/tests/controllers.tests/folder.test.ts b/src/tests/controllers.tests/folder.test.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/tests/controllers.tests/resume.test.ts b/src/tests/controllers.tests/resume.test.ts index e69de29..a904ff9 100644 --- a/src/tests/controllers.tests/resume.test.ts +++ b/src/tests/controllers.tests/resume.test.ts @@ -0,0 +1,88 @@ +import { dbConnect, dbDisconnect } from "../dbHandler"; +import { type resumeType } from "../../models/resume.model"; +import { type ActivitiesType } from "../../models/activities.model"; +import { resumeDummyData1, activityDummyData1 } from "./dummyData"; +import { + createResume, + getAllResumes, + getResumeById, + updateResume, + deleteResume, +} from "../../controllers/resume.controller"; +import { createActivity, deleteActivity } from "../../controllers/activities.controller"; +import { describe, test, expect, beforeEach, afterEach } from "vitest"; +import mongoose from "mongoose"; + +describe("Resume controller tests", () => { + beforeEach(async () => dbConnect()); + afterEach(async () => dbDisconnect()); + + test("Adds and retrieves a resumes", async () => { + await createResume(resumeDummyData1 as resumeType); + const returnedResumes = await getAllResumes(resumeDummyData1.user); + + //get back the 1 resumes that was added + expect(returnedResumes.length).to.equal(1); + expect(returnedResumes[0]).toMatchObject(resumeDummyData1); + + //Can't add duplicate name + await expect( + createResume(resumeDummyData1 as resumeType), + ).rejects.toThrowError(); + + const returnedResumes2 = await getAllResumes(resumeDummyData1.user); + + //if duplicate, shouldn't add to db + expect(returnedResumes2.length).to.equal(1); + + const returnedResumes3 = await getAllResumes("fakeuserid"); + + //don't get records for a different user id + expect(returnedResumes3.length).to.equal(0); + }); + + test("Finds, updates, and deletes a resume", async () => { + await createResume(resumeDummyData1 as resumeType); + const returnedRe = await getAllResumes(resumeDummyData1.user); + + const returnedResumes = await getResumeById( + resumeDummyData1.user, + returnedRe[0]._id, + ); + + expect(returnedResumes).toMatchObject(resumeDummyData1); + + const newItemName = "resumesItem2"; + await updateResume(resumeDummyData1.user, returnedRe[0]._id, { + ...resumeDummyData1, + itemName: newItemName, + } as resumeType); + const returnedResumes2 = await getResumeById( + resumeDummyData1.user, + returnedRe[0]._id, + ); + expect(returnedResumes2?.itemName).to.equal(newItemName); + + await deleteResume(resumeDummyData1.user, returnedRe[0]._id); + const returnedResumes3 = await getAllResumes(resumeDummyData1.user); + expect(returnedResumes3.length).to.equal(0); + + await expect( + updateResume(resumeDummyData1.user, "", {} as resumeType), + ).rejects.toThrowError("Missing"); + }); + + test("Correctly updates resume array upon item deletion", async () => { + const newActivity = await createActivity(activityDummyData1 as ActivitiesType); + let resumeDummyData2 = structuredClone(resumeDummyData1); + resumeDummyData2.itemIds = [newActivity._id]; + resumeDummyData2.templateId = new mongoose.Types.ObjectId("75e4f54db1e12e776e01cf31"); + + + const origResume = await createResume(resumeDummyData2 as resumeType); + await deleteActivity(activityDummyData1.user, newActivity._id); + const updatedResume = await getResumeById(origResume.user, origResume._id); + + expect(updatedResume?.itemIds).toHaveLength(0); + }) +}); From d08e989937d71898147e94574ce682195fe56012 Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Sun, 3 Mar 2024 17:22:14 -0600 Subject: [PATCH 36/37] implements section heading controller, router, and tests --- src/controllers/sectionHeading.controller.ts | 148 ++++++++++++++++++ src/routers/sectionHeading.router.ts | 119 ++++++++++++++ src/tests/controllers.tests/dummyData.ts | 6 +- .../controllers.tests/sectionHeading.test.ts | 71 +++++++++ 4 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 src/controllers/sectionHeading.controller.ts create mode 100644 src/routers/sectionHeading.router.ts create mode 100644 src/tests/controllers.tests/sectionHeading.test.ts diff --git a/src/controllers/sectionHeading.controller.ts b/src/controllers/sectionHeading.controller.ts new file mode 100644 index 0000000..e2dc68e --- /dev/null +++ b/src/controllers/sectionHeading.controller.ts @@ -0,0 +1,148 @@ +import { SectionHeadingModel, type SectionHeadingType } from "../models/sectionHeading.model"; +import { ResumeModel } from "../models/resume.model"; +import mongoose from "mongoose"; +import { HttpError, HttpStatus, checkMongooseErrors } from "../utils/errors"; +import { checkDuplicateItemName } from "../utils/checkDuplicates"; + +export const createSectionHeading = async (sectionHeadingsFields: SectionHeadingType) => { + try { + if(await checkDuplicateItemName(sectionHeadingsFields.itemName)){ + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Duplicate item name", + ) + } + + const newSectionHeadings = new SectionHeadingModel(sectionHeadingsFields); + await newSectionHeadings.save(); + return newSectionHeadings; + } catch (err: unknown) { + if (err instanceof HttpError) { + throw err; + } + + checkMongooseErrors(err); + + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "SectionHeading creation failed", + { cause: err }, + ); + } +}; + +export const getAllSectionHeadings = async (user: string) => { + try { + const sectionHeadings = await SectionHeadingModel.find({ user: user }); + return sectionHeadings; + } 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, + "SectionHeadings retrieval failed", + { cause: err }, + ); + } +} + +export const getSectionHeadingById = async (user: string, sectionHeadingId: string) => { + try { + const sectionHeading = await SectionHeadingModel.findOne({ + user: user, + _id: sectionHeadingId, + }); + return sectionHeading; + } 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, + "SectionHeading retrieval failed", + { cause: err }, + ); + } +}; + +export const updateSectionHeading = async ( + user: string, + sectionHeadingId: string, + sectionHeadingsFields: SectionHeadingType, +) => { + try { + if (!sectionHeadingId) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Missing sectionHeading ID for update", + ); + } + + if (await checkDuplicateItemName(sectionHeadingsFields.itemName, sectionHeadingId)) { + throw new HttpError(HttpStatus.BAD_REQUEST, "Duplicate item name"); + } + + const updatedSectionHeading = await SectionHeadingModel.findOneAndUpdate( + { _id: sectionHeadingId, user: user }, // Query to match the document by _id and user + { $set: sectionHeadingsFields }, // Update operation + { new: true, runValidators: true }, // Options: return the updated document and run schema validators + ); + return updatedSectionHeading; + } 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, + "SectionHeading update failed", + { cause: err }, + ); + } +}; + +export const deleteSectionHeading = async (user: string, sectionHeadingId: string) => { + try { + await ResumeModel.updateMany( + { itemIds: new mongoose.Types.ObjectId(sectionHeadingId) }, + { $pull: { itemIds: new mongoose.Types.ObjectId(sectionHeadingId) } } + ); + + const deletedSectionHeading = await SectionHeadingModel.findOneAndDelete({ + _id: sectionHeadingId, + user: user, + }); + if (!deletedSectionHeading) { + throw new HttpError( + HttpStatus.NOT_FOUND, + "SectionHeading not found or already deleted", + ); + } + return { message: "SectionHeading 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, + "SectionHeading deletion failed", + { cause: err }, + ); + } +}; diff --git a/src/routers/sectionHeading.router.ts b/src/routers/sectionHeading.router.ts new file mode 100644 index 0000000..1f6d3b1 --- /dev/null +++ b/src/routers/sectionHeading.router.ts @@ -0,0 +1,119 @@ +import { Router, type Request, type Response } from "express"; +import { + createSectionHeading, + getAllSectionHeadings, + getSectionHeadingById, + updateSectionHeading, + deleteSectionHeading, +} from "../controllers/sectionHeading.controller"; +import { HttpError, HttpStatus } from "../utils/errors"; +import { type SectionHeadingType } from "../models/sectionHeading.model"; + +export const sectionHeadingRouter = Router(); + +//Add an sectionHeading +//Note that the user field (which is part of SectionHeadingType) in body is automatically populated by verifyToken middleware +sectionHeadingRouter.post( + "/", + async (req: Request, res: Response) => { + try { + const sectionHeading = await createSectionHeading(req.body); + res.status(HttpStatus.OK).json(sectionHeading); + } 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 sectionHeading +sectionHeadingRouter.get( + "/", + async (req: Request, res: Response) => { + try { + const sectionHeading = await getAllSectionHeadings(req.body.user); + res.status(HttpStatus.OK).json(sectionHeading); + } 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 sectionHeading by id +sectionHeadingRouter.get( + "/:sectionHeadingId", + async (req: Request, res: Response) => { + try { + const sectionHeading = await getSectionHeadingById( + req.body.user, + req.params.sectionHeadingId, + ); + res.status(HttpStatus.OK).json(sectionHeading); + } 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 sectionHeading +sectionHeadingRouter.put( + "/:sectionHeadingId", + async (req: Request, res: Response) => { + try { + const sectionHeading = await updateSectionHeading( + req.body.user, + req.params.sectionHeadingId, + req.body, + ); + res.status(HttpStatus.OK).json(sectionHeading); + } 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 sectionHeading +sectionHeadingRouter.delete( + "/:sectionHeadingId", + async (req: Request, res: Response) => { + try { + const sectionHeading = await deleteSectionHeading( + req.body.user, + req.params.sectionHeadingId, + ); + res.status(HttpStatus.OK).json(sectionHeading); + } 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 794a545..58d8272 100644 --- a/src/tests/controllers.tests/dummyData.ts +++ b/src/tests/controllers.tests/dummyData.ts @@ -64,4 +64,8 @@ export const resumeDummyData1 = { templateId: new mongoose.Types.ObjectId("75e4f54db1e12e776e01cf31"), } - +export const sectionHeadingDummyData1 = { + user: "test", + itemName: "sectionHeadingItem1", + title: "test section heading", +} diff --git a/src/tests/controllers.tests/sectionHeading.test.ts b/src/tests/controllers.tests/sectionHeading.test.ts new file mode 100644 index 0000000..5c5879e --- /dev/null +++ b/src/tests/controllers.tests/sectionHeading.test.ts @@ -0,0 +1,71 @@ +import { dbConnect, dbDisconnect } from "../dbHandler"; +import { type SectionHeadingType } from "../../models/sectionHeading.model"; +import { sectionHeadingDummyData1 } from "./dummyData"; +import { + createSectionHeading, + getAllSectionHeadings, + getSectionHeadingById, + updateSectionHeading, + deleteSectionHeading, +} from "../../controllers/sectionHeading.controller"; +import { describe, test, expect, beforeEach, afterEach } from "vitest"; + +describe("SectionHeadings controller tests", () => { + beforeEach(async () => dbConnect()); + afterEach(async () => dbDisconnect()); + + test("Adds and retrieves an sectionHeadings", async () => { + await createSectionHeading(sectionHeadingDummyData1 as SectionHeadingType); + const returnedSectionHeadings = await getAllSectionHeadings(sectionHeadingDummyData1.user); + + //get back the 1 sectionHeadings that was added + expect(returnedSectionHeadings.length).to.equal(1); + expect(returnedSectionHeadings[0]).toMatchObject(sectionHeadingDummyData1); + + //Can't add duplicate name + await expect( + createSectionHeading(sectionHeadingDummyData1 as SectionHeadingType), + ).rejects.toThrowError(); + + const returnedSectionHeadings2 = await getAllSectionHeadings(sectionHeadingDummyData1.user); + + //if duplicate, shouldn't add to db + expect(returnedSectionHeadings2.length).to.equal(1); + + const returnedSectionHeadings3 = await getAllSectionHeadings("fakeuserid"); + + //don't get records for a different user id + expect(returnedSectionHeadings3.length).to.equal(0); + }); + + test("Finds, updates, and deletes an sectionHeadings", async () => { + await createSectionHeading(sectionHeadingDummyData1 as SectionHeadingType); + const returnedEd = await getAllSectionHeadings(sectionHeadingDummyData1.user); + + const returnedSectionHeadings = await getSectionHeadingById( + sectionHeadingDummyData1.user, + returnedEd[0]._id, + ); + + expect(returnedSectionHeadings).toMatchObject(sectionHeadingDummyData1); + + const newItemName = "sectionHeadingsItem2"; + await updateSectionHeading(sectionHeadingDummyData1.user, returnedEd[0]._id, { + ...sectionHeadingDummyData1, + itemName: newItemName, + } as SectionHeadingType); + const returnedSectionHeadings2 = await getSectionHeadingById( + sectionHeadingDummyData1.user, + returnedEd[0]._id, + ); + expect(returnedSectionHeadings2?.itemName).to.equal(newItemName); + + await deleteSectionHeading(sectionHeadingDummyData1.user, returnedEd[0]._id); + const returnedSectionHeadings3 = await getAllSectionHeadings(sectionHeadingDummyData1.user); + expect(returnedSectionHeadings3.length).to.equal(0); + + await expect( + updateSectionHeading(sectionHeadingDummyData1.user, "", {} as SectionHeadingType), + ).rejects.toThrowError("Missing"); + }); +}); From a3073b0be948fdbf95b880aa445bee2d9ac72d8b Mon Sep 17 00:00:00 2001 From: Jack Garritano <114269804+jackgarritano@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:42:53 -0600 Subject: [PATCH 37/37] adds section heading to root router --- src/routers/root.router.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routers/root.router.ts b/src/routers/root.router.ts index 81b4e19..e3b4f99 100644 --- a/src/routers/root.router.ts +++ b/src/routers/root.router.ts @@ -6,6 +6,7 @@ import { experienceRouter } from "./experience.router"; import { headingRouter } from "./heading.router"; import { projectRouter } from "./project.router"; import { resumeRouter } from "./resume.router"; +import { sectionHeadingRouter } from "./sectionHeading.router"; export const router = Router(); @@ -16,3 +17,4 @@ router.use("/experience", experienceRouter); router.use("/headings", headingRouter); router.use("/projects", projectRouter); router.use("/resumes", resumeRouter); +router.use("/sectionHeadings", sectionHeadingRouter);