diff --git a/backend/src/app.ts b/backend/src/app.ts index 4d9588a4..49e02fe1 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -2,6 +2,8 @@ import { json } from "body-parser"; import express from "express"; import mongoose from "mongoose"; +import programFormRoutes from "../src/routes/program-form"; + import { mongoURI, port } from "./config"; /** @@ -24,6 +26,7 @@ void mongoose }); server.app.use(json()); +server.app.use("/api/programForm", programFormRoutes); // make server listen on some port server.app.listen(port, () => { diff --git a/backend/src/controllers/program-form.ts b/backend/src/controllers/program-form.ts new file mode 100644 index 00000000..5f14cb98 --- /dev/null +++ b/backend/src/controllers/program-form.ts @@ -0,0 +1,107 @@ +// copied from onboarding repo - TODO +// only need create + +/** + * Functions that process task route requests. + */ + +import { RequestHandler } from "express"; +import { validationResult } from "express-validator"; + +import ProgramFormModel from "../models/program-form"; +import validationErrorParser from "../util/validationErrorParser"; + +export type typeProgramForm = { + name: string; + abbreviation: string; + type: string; + startDate: Date; + endDate: Date; + color: number; +}; + +/** + * This is an example of an Express API request handler. We'll tell Express to + * run this function when our backend receives a request to retrieve a + * particular task. + * + * Request handlers typically have 3 parameters: req, res, and next. + * + * @param req The Request object from Express. This contains all the data from + * the API request. (https://expressjs.com/en/4x/api.html#req) + * @param res The Response object from Express. We use this to generate the API + * response for Express to send back. (https://expressjs.com/en/4x/api.html#res) + * @param next The next function in the chain of middleware. If there's no more + * processing we can do in this handler, but we're not completely done handling + * the request, then we can pass it along by calling next(). For all of the + * handlers defined in `src/controllers`, the next function is the global error + * handler in `src/app.ts`. + */ +// export const getForm: RequestHandler = async (req, res, next) => { +// const { id } = req.params; + +// try { +// // if the ID doesn't exist, then findById returns null +// const task = await TaskModel.findById(id); + +// if (task === null) { +// throw createHttpError(404, "Task not found."); +// } + +// // Set the status code (200) and body (the task object as JSON) of the response. +// // Note that you don't need to return anything, but you can still use a return +// // statement to exit the function early. +// res.status(200).json(task); +// } catch (error) { +// // pass errors to the error handler +// next(error); +// } +// }; + +export const createForm: RequestHandler = async (req, res, next) => { + // extract any errors that were found by the validator + const errors = validationResult(req); + /* eslint-disable */ + const { name, abbreviation, type, startDate, endDate, color } = req.body; + /* eslint-enable */ + + try { + // if there are errors, then this function throws an exception + validationErrorParser(errors); //errors + + // if (!errors.isEmpty()) { + // let errorString = ""; + // for (const error of errors.array()) { + // errorString += error.msg + " "; + // } + // throw new ValidationError(errorString); + // } + + const programForm = await ProgramFormModel.create({ + name: String, + abbreviation: String, + type: String, + startDate: Date, + endDate: Date, + color: Number, + }); + + // 201 means a new resource has been created successfully + // the newly created task is sent back to the user + res.status(201).json(programForm); + } catch (error) { + next(error); + } +}; + +// export const removeForm: RequestHandler = async (req, res, next) => { +// const { id } = req.params; + +// try { +// const result = await TaskModel.deleteOne({ _id: id }); + +// res.status(200).json(result); +// } catch (error) { +// next(error); +// } +// }; diff --git a/backend/src/models/program-form.ts b/backend/src/models/program-form.ts new file mode 100644 index 00000000..e0764bfe --- /dev/null +++ b/backend/src/models/program-form.ts @@ -0,0 +1,16 @@ +//modified from onboarding repo for program form backend + +import { InferSchemaType, Schema, model } from "mongoose"; + +const programFormSchema = new Schema({ + name: { type: String, required: true }, + abbreviation: { type: String, required: true }, // e.g. ENTR + type: { type: String, required: true }, // regular vs. varying + startDate: { type: Date, required: true }, + endDate: { type: Date, required: true }, + color: { type: Number, enum: [1, 2, 3, 4], required: true }, // options: 1 (teal, #4FA197), 2 (yellow, #FFB800), 3 (pink, #FF7A5E), 4 (olive, #B6BF0E) +}); + +type ProgramForm = InferSchemaType; + +export default model("ProgramForm", programFormSchema); diff --git a/backend/src/routes/program-form.ts b/backend/src/routes/program-form.ts new file mode 100644 index 00000000..49db6d62 --- /dev/null +++ b/backend/src/routes/program-form.ts @@ -0,0 +1,19 @@ +/** + * Task route requests. + */ + +import express from "express"; // { RequestHandler } + +// import { ParamsDictionary } from "express-serve-static-core"; +// import { ParsedQs } from "qs"; + +import * as ProgramFormController from "../controllers/program-form"; +import * as ProgramFormValidator from "../validators/program-form"; + +const router = express.Router(); + +//router.get("/:id", ProgramFormController.getForm); +router.post("/", ProgramFormValidator.createForm, ProgramFormController.createForm); +//router.delete("/:id", ProgramFormController.removeForm); + +export default router; diff --git a/backend/src/util/validationErrorParser.ts b/backend/src/util/validationErrorParser.ts new file mode 100644 index 00000000..844fe849 --- /dev/null +++ b/backend/src/util/validationErrorParser.ts @@ -0,0 +1,25 @@ +import { Result, ValidationError } from "express-validator"; +import createHttpError from "http-errors"; + +/** + * Parses through errors thrown by validator (if any exist). Error messages are + * added to a string and that string is used as the error message for the HTTP + * error. + * + * @param errors the validation result provided by express validator middleware + */ +const validationErrorParser = (errors: Result) => { + if (!errors.isEmpty()) { + let errorString = ""; + + // parse through errors returned by the validator and append them to the error string + for (const error of errors.array()) { + errorString += error.msg + " "; + } + + // trim removes the trailing space created in the for loop + throw createHttpError(400, errorString.trim()); + } +}; + +export default validationErrorParser; diff --git a/backend/src/validators/program-form.ts b/backend/src/validators/program-form.ts new file mode 100644 index 00000000..4a36aa85 --- /dev/null +++ b/backend/src/validators/program-form.ts @@ -0,0 +1,88 @@ +import { body } from "express-validator"; + +import { typeProgramForm } from "../controllers/program-form"; + +// more info about validators: +// https://express-validator.github.io/docs/guides/validation-chain +// https://github.com/validatorjs/validator.js#validators + +// const makeIDValidator = () => +// body("_id") +// .exists() +// .withMessage("_id is required") +// .bail() +// .isMongoId() +// .withMessage("_id must be a MongoDB object ID"); +// const makeTitleValidator = () => +// body("title") +// // title must exist, if not this message will be displayed +// .exists() +// .withMessage("title is required") +// // bail prevents the remainder of the validation chain for this field from being executed if +// // there was an error +// .bail() +// .isString() +// .withMessage("title must be a string") +// .bail() +// .notEmpty() +// .withMessage("title cannot be empty"); +const makeNameValidator = () => body("name").isString().withMessage("name must be a string"); +const makeAbbreviationValidator = () => + body("abbreviation").isString().withMessage("abbreviation must be a string"); +const makeTypeValidator = () => body("type").isString().withMessage("type must be a string"); +const makeStartDateValidator = () => + body("startDate").isISO8601().withMessage("startDate must be a valid date-time string"); +const makeEndDateValidator = () => + body("endDate") + .isISO8601() + .custom((value, { req }) => { + const reqBody: typeProgramForm = req.body as typeProgramForm; + if (value < reqBody.startDate) throw new Error("end date must be after start date"); + return true; + }); +const makeColorValidator = () => + body("color") + .isNumeric() + .custom((value) => { + if (value < 1 || value > 4) { + throw new Error("color must be an option number 1-4"); + } + return true; + }); + +// assignee is for Part 2.1 +// const makeAssigneeValidator = () => +// body("assignee").optional().isMongoId().withMessage("assignee must be a MongoDB object ID"); + +// establishes a set of rules that the body of the task creation route must follow +export const createForm = [ + makeNameValidator(), + makeAbbreviationValidator(), + makeTypeValidator(), + makeStartDateValidator(), + makeEndDateValidator(), + makeColorValidator(), +]; + +// export function createForm() { +// makeIDValidator(); +// makeNameValidator(); +// makeAbbreviationValidator(); +// makeTypeValidator(); +// makeStartDateValidator(); +// makeEndDateValidator(); +// makeColorValidator(); +// } + +// export const updateForm = [ +// // makeIDValidator(), +// // makeTitleValidator(), +// // makeDescriptionValidator(), +// // makeIsCheckedValidator(), +// // makeDateCreatedValidator(), +// // makeAssigneeValidator(), // for Part 2.1 +// ]; + +// export function editForm(arg0: string, editForm: any, createForm: RequestHandler>) { +// throw new Error("Function not implemented."); +// } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..7b5881df --- /dev/null +++ b/package-lock.json @@ -0,0 +1,37 @@ +{ + "name": "PIA-Program-Manager", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "express-validator": "^7.0.1" + } + }, + "node_modules/express-validator": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.0.1.tgz", + "integrity": "sha512-oB+z9QOzQIE8FnlINqyIFA8eIckahC6qc8KtqLdLJcU3/phVyuhXH3bA4qzcrhme+1RYaCSwrq+TlZ/kAKIARA==", + "dependencies": { + "lodash": "^4.17.21", + "validator": "^13.9.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..d69a0f83 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "express-validator": "^7.0.1" + } +}