diff --git a/backend/package-lock.json b/backend/package-lock.json index 584d3e17..ea0acb80 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -4055,13 +4055,13 @@ } }, "node_modules/mongoose": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.0.3.tgz", - "integrity": "sha512-LJRT0yP4TW14HT4r2RkxqyvoTylMSzWpl5QOeVHTnRggCLQSpkoBdgbUtORFq/mSL2o9cLCPJz+6uzFj25qbHw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.1.0.tgz", + "integrity": "sha512-kOA4Xnq2goqNpN9EmYElGNWfxA9H80fxcr7UdJKWi3UMflza0R7wpTihCpM67dE/0MNFljoa0sjQtlXVkkySAQ==", "dependencies": { "bson": "^6.2.0", "kareem": "2.5.1", - "mongodb": "6.2.0", + "mongodb": "6.3.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -4075,97 +4075,11 @@ "url": "https://opencollective.com/mongoose" } }, - "node_modules/mongoose/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==", - "dependencies": { - "@types/node": "*", - "@types/webidl-conversions": "*" - } - }, - "node_modules/mongoose/node_modules/mongodb": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.2.0.tgz", - "integrity": "sha512-d7OSuGjGWDZ5usZPqfvb36laQ9CPhnWkAGHT61x5P95p/8nMVeH8asloMwW6GcYFeB0Vj4CB/1wOTDG2RA9BFA==", - "dependencies": { - "@mongodb-js/saslprep": "^1.1.0", - "bson": "^6.2.0", - "mongodb-connection-string-url": "^2.6.0" - }, - "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, - "node_modules/mongoose/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==", - "dependencies": { - "@types/whatwg-url": "^8.2.1", - "whatwg-url": "^11.0.0" - } - }, "node_modules/mongoose/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "node_modules/mongoose/node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/mongoose/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==", - "dependencies": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/mpath": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", diff --git a/backend/src/app.ts b/backend/src/app.ts index 6d8234e2..afc514dc 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -7,6 +7,7 @@ import mongoose from "mongoose"; import { mongoURI, port } from "./config"; import { errorHandler } from "./errors/handler"; import router from "./routes/api"; +import program from "./routes/program"; /** * Express server application class @@ -30,6 +31,8 @@ void mongoose // Middleware server.app.use(json()); +server.app.use("/program", program); +server.app.use(errorHandler); // sets the "Access-Control-Allow-Origin" header on all responses to allow server.app.use(cors()); diff --git a/backend/src/controllers/.keep b/backend/src/controllers/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/src/controllers/program.ts b/backend/src/controllers/program.ts new file mode 100644 index 00000000..03f42655 --- /dev/null +++ b/backend/src/controllers/program.ts @@ -0,0 +1,30 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ +import { RequestHandler } from "express"; +import { validationResult } from "express-validator"; + +import ProgramFormModel from "../models/program-form"; +import validationErrorParser from "../util/validationErrorParser"; + +export type Program = { + name: string; + abbreviation: string; + type: string; + daysOfWeek: string[]; + startDate: string; + endDate: string; + color: string; //colorValueHex; +}; + +export const createForm: RequestHandler = async (req, res, next) => { + const errors = validationResult(req); + + try { + validationErrorParser(errors); + + const programForm = await ProgramFormModel.create(req.body as Program); + + res.status(201).json(programForm); + } catch (error) { + next(error); + } +}; diff --git a/backend/src/errors/handler.ts b/backend/src/errors/handler.ts index d55e5774..cef88cd8 100644 --- a/backend/src/errors/handler.ts +++ b/backend/src/errors/handler.ts @@ -1,4 +1,4 @@ -import { Request, Response } from "express"; +import { NextFunction, Request, Response } from "express"; import { CustomError } from "./errors"; import { InternalError } from "./internal"; @@ -6,7 +6,7 @@ import { InternalError } from "./internal"; /** * Generic Error Handler */ -export const errorHandler = (err: Error, req: Request, res: Response) => { +export const errorHandler = (err: Error, _req: Request, res: Response, _nxt: NextFunction) => { if (!err) return; if (err instanceof CustomError && !(err instanceof InternalError)) { console.log(err.displayMessage(true)); diff --git a/backend/src/models/program-form.ts b/backend/src/models/program-form.ts new file mode 100644 index 00000000..c77b4760 --- /dev/null +++ b/backend/src/models/program-form.ts @@ -0,0 +1,17 @@ +//modified from onboarding repo for program form backend + +import { InferSchemaType, Schema, model } from "mongoose"; + +const programSchema = new Schema({ + name: { type: String, required: true }, + abbreviation: { type: String, required: true }, // e.g. ENTR + type: { type: String, required: true }, // regular vs. varying + daysOfWeek: { type: [String], required: true }, // M, T, W, TH, F + startDate: { type: Date, required: true }, + endDate: { type: Date, required: true }, + color: { type: String, required: true }, // options: 1 (teal, #4FA197), 2 (yellow, #FFB800), 3 (pink, #FF7A5E), 4 (olive, #B6BF0E) +}); + +type ProgramForm = InferSchemaType; + +export default model("ProgramForm", programSchema); diff --git a/backend/src/routes/program.ts b/backend/src/routes/program.ts new file mode 100644 index 00000000..f043d540 --- /dev/null +++ b/backend/src/routes/program.ts @@ -0,0 +1,14 @@ +/** + * Task route requests. + */ + +import express from "express"; // { RequestHandler } + +import * as ProgramController from "../controllers/program"; +import * as ProgramValidator from "../validators/program"; + +const router = express.Router(); + +router.post("/", ProgramValidator.createForm, ProgramController.createForm); + +export default router; diff --git a/backend/src/validators/program.ts b/backend/src/validators/program.ts new file mode 100644 index 00000000..488f214c --- /dev/null +++ b/backend/src/validators/program.ts @@ -0,0 +1,114 @@ +import { body } from "express-validator"; + +import { Program } from "../controllers/program"; + +const makeNameValidator = () => + body("name") + .exists() + .withMessage("name needed") + .bail() + .isString() + .withMessage("name must be a string") + .bail() + .notEmpty() + .withMessage("name must not be empty"); +const makeAbbreviationValidator = () => + body("abbreviation") + .exists() + .withMessage("abbreviation needed") + .bail() + .isString() + .withMessage("abbreviation must be a string") + .bail() + .notEmpty() + .withMessage("abbreviation must not be empty"); +const makeTypeValidator = () => + body("type") + .exists() + .withMessage("type needed") + .bail() + .isString() + .withMessage("type must be a string") + .bail() + .notEmpty() + .withMessage("type must not be empty") + .custom((value: string) => { + if (value !== "regular" && value !== "varying") + throw new Error("program type must be regular or varying"); + return true; + }); +const makeDaysOfWeekValidator = () => + body("daysOfWeek") + .exists() + .withMessage("days of week selection needed") + .bail() + .isArray() + .withMessage("days of week selection must be an array") + .custom((value: string[]) => { + if (value.length === 0) throw new Error("days of week selection needed"); + for (const valuei of value) { + if (valuei !== "M" && valuei !== "T" && valuei !== "W" && valuei !== "TH" && valuei !== "F") + throw new Error("days of week selection must be M, T, W, TH, or F"); + } + return true; + }); +const makeStartDateValidator = () => + body("startDate") + .exists() + .withMessage("start date needed") + .bail() + .isISO8601() + .withMessage("start date must be a valid date-time string"); +const makeEndDateValidator = () => + body("endDate") + .exists() + .withMessage("end date needed") + .bail() + .isISO8601() + .withMessage("end date must be a valid date-time string") + .bail() + .custom((value: string, { req }) => { + const reqBody: Program = req.body as Program; + if (new Date(value) < new Date(reqBody.startDate)) + throw new Error("end date must be after start date"); + return true; + }); +const makeColorValidator = () => + body("color") + .exists() + .withMessage("color needed") + .bail() + .isString() + .withMessage("color hex should be string") + .isLength({ min: 7, max: 7 }) + .withMessage("color hex should have 7 characters") + .bail() + .notEmpty() + .withMessage("color hex required") + .bail() + .custom((value: string) => { + if (value.length !== 7) throw new Error("color hex should have 7 characters"); + if (!value.startsWith("#")) throw new Error("color hex should start with #"); + for (let i = 1; i <= 6; i++) { + const code = value.charCodeAt(i); + if ( + !(code > 47 && code < 58) && // numeric (0-9) + !(code > 64 && code < 91) + ) { + // upper alpha (A-Z) + throw new Error("color hex must be a hex string, with characters 0-9 or A-F"); + } + } + return true; + }); +// check for first chara being # and others being 1-F + +export const createForm = [ + makeNameValidator(), + makeAbbreviationValidator(), + makeTypeValidator(), + makeDaysOfWeekValidator(), + makeStartDateValidator(), + makeEndDateValidator(), + makeColorValidator(), +]; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 183d6ba2..edee9ab2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -25,7 +25,7 @@ "react-day-picker": "^8.10.0", "react-dom": "^18", "react-hook-form": "^7.49.3", - "tailwind-merge": "^2.2.0", + "tailwind-merge": "^2.2.1", "tailwindcss": "^3.4.1", "tailwindcss-animate": "^1.0.7", "ts-jest": "^29.1.1" @@ -795,7 +795,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.6", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -3861,6 +3863,8 @@ }, "node_modules/clsx": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", "license": "MIT", "engines": { "node": ">=6" @@ -8347,10 +8351,11 @@ } }, "node_modules/tailwind-merge": { - "version": "2.2.0", - "license": "MIT", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.1.tgz", + "integrity": "sha512-o+2GTLkthfa5YUt4JxPfzMIpQzZ3adD1vLVkvKE1Twl9UAhGsEbIZhHHZVRttyW177S8PDJI3bTQNaebyofK3Q==", "dependencies": { - "@babel/runtime": "^7.23.5" + "@babel/runtime": "^7.23.7" }, "funding": { "type": "github",