Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import express from "express";
import { isHttpError } from "http-errors";
import taskRoutes from "src/routes/task";
import tasksRoutes from "src/routes/tasks";
import userRoutes from "src/routes/user";

import type { NextFunction, Request, Response } from "express";

Expand All @@ -29,6 +30,7 @@ app.use(

app.use("/api/task", taskRoutes);
app.use("/api/tasks", tasksRoutes);
app.use("/api/user", userRoutes);

/**
* Error handler; all errors thrown by server are handled here.
Expand Down
64 changes: 25 additions & 39 deletions backend/src/controllers/task.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,59 @@
/**
* Functions that process task route requests.
*/

import { validationResult } from "express-validator";
import createHttpError from "http-errors";
import TaskModel from "src/models/task";
import validationErrorParser from "src/util/validationErrorParser";

import type { RequestHandler } from "express";

/**
* 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 getTask: 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);
const task = await TaskModel.findById(id).populate("assignee");

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);
}
};

// Define a custom type for the request body so we can have static typing
// for the fields
type CreateTaskBody = {
title: string;
description?: string;
isChecked?: boolean;
assignee?: string;
};

type UpdateTaskBody = {
title: string;
description?: string;
isChecked?: boolean;
assignee?: string;
};

export const createTask: RequestHandler = async (req, res, next) => {
// extract any errors that were found by the validator
const errors = validationResult(req);
const { title, description, isChecked } = req.body as CreateTaskBody;
// Extract assignee along with other fields
const { title, description, isChecked, assignee } = req.body as CreateTaskBody;

try {
// if there are errors, then this function throws an exception
validationErrorParser(errors);

const task = await TaskModel.create({
title,
description,
isChecked,
assignee, // Save the assignee
dateCreated: Date.now(),
});

// 201 means a new resource has been created successfully
// the newly created task is sent back to the user
// Populate the assignee so the frontend gets the user object back immediately
await task.populate("assignee");

res.status(201).json(task);
} catch (error) {
next(error);
Expand All @@ -98,26 +73,37 @@ export const removeTask: RequestHandler = async (req, res, next) => {
};

export const updateTask: RequestHandler = async (req, res, next) => {
// extract any errors that were found by the validator
const errors = validationResult(req);
const { isChecked, _id } = req.body as UpdateTaskBody & { _id: string };
// Extract ALL fields: title, description, assignee, isChecked
const { title, description, assignee, isChecked, _id } = req.body as UpdateTaskBody & {
_id: string;
};
const { id } = req.params;

try {
// if there are errors, then this function throws an exception
validationErrorParser(errors);

if (_id && id !== _id) {
throw createHttpError(404, "Task ID Mismatch.");
}

const task = await TaskModel.findByIdAndUpdate(id, { isChecked });
// Update all fields in the database
const task = await TaskModel.findByIdAndUpdate(
id,
{
title,
description,
assignee,
isChecked,
},
{ new: true }, // Optional: returns the modified document
);

if (task === null) {
throw createHttpError(404, "Task not found.");
}

const updatedTask = await TaskModel.findById(id);
const updatedTask = await TaskModel.findById(id).populate("assignee");

res.status(200).json(updatedTask);
} catch (error) {
Expand Down
3 changes: 1 addition & 2 deletions backend/src/controllers/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import type { RequestHandler } from "express";

export const getAllTasks: RequestHandler = async (req, res, next) => {
try {
// your code here
const sortedTasks = await TaskModel.find().sort({ dateCreated: "desc" });
const sortedTasks = await TaskModel.find().populate("assignee").sort({ dateCreated: "desc" });

res.status(200).json(sortedTasks);
} catch (error) {
Expand Down
124 changes: 124 additions & 0 deletions backend/src/controllers/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Functions that process task route requests.
*/

import { validationResult } from "express-validator";
import createHttpError from "http-errors";
import UserModel from "src/models/user";
import validationErrorParser from "src/util/validationErrorParser";

import type { RequestHandler } from "express";

/**
* 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 getUser: RequestHandler = async (req, res, next) => {
const { id } = req.params;

try {
// if the ID doesn't exist, then findById returns null
const user = await UserModel.findById(id);

if (user === null) {
throw createHttpError(404, "User 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(user);
} catch (error) {
// pass errors to the error handler
next(error);
}
};

// Define a custom type for the request body so we can have static typing
// for the fields
type CreateUserBody = {
name: string;
profilePictureURL?: string;
};

type UpdateUserBody = {
name: string;
profilePictureURL?: string;
};

export const createUser: RequestHandler = async (req, res, next) => {
// extract any errors that were found by the validator
const errors = validationResult(req);
const { name, profilePictureURL } = req.body as CreateUserBody;

try {
// if there are errors, then this function throws an exception
validationErrorParser(errors);

const user = await UserModel.create({
name,
profilePictureURL,
});

// 201 means a new resource has been created successfully
// the newly created task is sent back to the user
res.status(201).json(user);
} catch (error) {
next(error);
}
};

export const removeUser: RequestHandler = async (req, res, next) => {
const { id } = req.params;

try {
const result = await UserModel.deleteOne({ _id: id });

res.status(200).json(result);
} catch (error) {
next(error);
}
};

export const updateUser: RequestHandler = async (req, res, next) => {
// extract any errors that were found by the validator
const errors = validationResult(req);

//CHANGE THIS LINE EVENTUALLY
const { name, _id } = req.body as UpdateUserBody & { _id: string };
const { id } = req.params;

try {
// if there are errors, then this function throws an exception
validationErrorParser(errors);

if (_id && id !== _id) {
throw createHttpError(404, "User ID Mismatch.");
}

const task = await UserModel.findByIdAndUpdate(id, { name });

if (task === null) {
throw createHttpError(404, "Task not found.");
}

const updatedUser = await UserModel.findById(id);

res.status(200).json(updatedUser);
} catch (error) {
next(error);
}
};
2 changes: 2 additions & 0 deletions backend/src/models/task.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ObjectId } from "mongodb";
import { model, Schema } from "mongoose";

import type { InferSchemaType } from "mongoose";
Expand All @@ -12,6 +13,7 @@ const taskSchema = new Schema({
// When we send a Task object in the JSON body of an API response, the date
// will automatically get "serialized" into a standard date string.
dateCreated: { type: Date, required: true },
assignee: { type: ObjectId, ref: "User" },
});

type Task = InferSchemaType<typeof taskSchema>;
Expand Down
12 changes: 12 additions & 0 deletions backend/src/models/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { model, Schema } from "mongoose";

import type { InferSchemaType } from "mongoose";

const userSchema = new Schema({
name: { type: String, required: true },
profilePictureURL: { type: String },
});

type User = InferSchemaType<typeof userSchema>;

export default model<User>("User", userSchema);
24 changes: 24 additions & 0 deletions backend/src/routes/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* User route requests.
*/

import express from "express";
import * as UserController from "src/controllers/user";
import * as UserValidator from "src/validators/user";

const router = express.Router();

router.get("/:id", UserController.getUser);

/**
* UserValidator.createUser serves as middleware for this route. This means
* that instead of immediately serving up the route when the request is made,
* Express firsts passes the request to UserValidator.createUser.
* UserValidator.createUser processes the request and determines whether the
* request should be sent through or an error should be thrown.
*/
router.post("/", UserValidator.createUser, UserController.createUser);
router.put("/:id", UserValidator.updateUser, UserController.updateUser);
router.delete("/:id", UserController.removeUser);

export default router;
42 changes: 42 additions & 0 deletions backend/src/validators/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { body } from "express-validator";

// 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 makeNameValidator = () =>
body("name")
// name must exist, if not this message will be displayed
.exists()
.withMessage("name is required")
// bail prevents the remainder of the validation chain for this field from being executed if
// there was an error
.bail()
.isString()
.withMessage("name must be a string")
.bail()
.notEmpty()
.withMessage("name cannot be empty");
const makeProfilePictureURLValidator = () =>
body("profilePictureURL")
// order matters for the validation chain - by marking this field as optional, the rest of
// the chain will only be evaluated if it exists
.optional()
.isString()
.withMessage("profilePictureURL must be a string");

// establishes a set of rules that the body of the task creation route must follow
export const createUser = [makeNameValidator(), makeProfilePictureURLValidator()];

export const updateUser = [
makeIDValidator(),
makeNameValidator(),
makeProfilePictureURLValidator(),
];
10 changes: 10 additions & 0 deletions frontend/public/userIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading