Storing Data in a MongoDB Database
1 Introduction
There are two main categories of databases: relational databases and non-relational databases. Relational databases
such as MySQL, SQL Server, and Postgre, store data in tables containing records of the same type, e.g., a table called
courses would contain records representing all the courses, and a users table would contain all the users of an
application. Records are represented as rows in the tables where each column stores data for attributes specifific to the
type of the table, e.g., the rows in the courses table might have columns such as name, description, startDate, endDate,
etc. Some of the columns might refer, or relate to other records in other tables such as the instructor column in the
courses table might refer to, or relate to a particular row in the users table signifying that that particular user is the
instructor of that particular course. Rows in one table relating to rows in another table is where relational databases get
their name. The structured query language or SQL, is a computer language commonly used to interact with relational
databases. The query in SQL generally means to ask for, or retrieve data that matches some criteria, often written as a
boolean expression or predicate.
More recently there has been a growing interest in representing and storing data using alternative strategies which have
collectively come to be referred to as non relational databases, or NoSQL databases. Non relational databases such as
MongoDB, Firebase, and Couchbase, store their data in collections containing documents which are roughly analogous to
tables and records in their relational counterparts. The biggest difference though is that the columns, or fifields in the rows
in relational databases generally can only contain primitive data types, e.g., simple strings, numbers, dates, and booleans,
whereas the fifields, or properties in non relational documents can be arbitrarily complex data types, e.g., strings, numbers,
booleans, dates as well as combinations of these in complex objects containing arrays of objects of arrays, etc. The other
big difference is that relational databases require the structure, or schema of the data to be explicitly described before
storing any data, whereas non relational databases do not require predefifined schemas. Instead, non relational databases
delegate this responsibility to the applications using the database. The structure, or schema in relational databases is
where structured query language gets its name.
In the previous chapter we learned how to create an HTTP server with Node.js and integrated it with a React.js Web user
interface application to store the application state on the server. In this chapter we expand on this idea to store the data to
MongoDB, a popular non relational database. The fifirst section demonstrates how to download, install and use a local
instance of the MongoDB database. The next section covers how to use the Mongoose library to integrate and program a
MongoDB database with a Node.js server application. The fifinal section describes how to deploy the database to Mongo
Atlas, a remote MongoDB database hosted as a cloud service.
The following fifigure illustrates the overall architecture of what we'll be building in this chapter. From right to left, we'll fifirst
create a MongoDB database called kanbas where we'll create several collections such as users, courses, modules,
assignments, etc. We'll use the Mongoose library to connect to the database programmatically from a Node server. A
Mongoose schema will describe the structure of the collections in the MongoDB database and a Mongoose model will
implement generic CRUD operations. We'll create higher level functions in Data Access Objects (DAOs) that operate on the
database, and expose those operations through Express routes as RESTful Web APIs. A React Web app will integrate with
the RESTful API through a client that will allow the user interface to interact with the database.2 Working with a Local MongoDB Instance
MongoDB is one of an increasingly popular family of non relational databases. Data is stored in collections of documents
usually formatted as JSON objects which makes it very convenient to integrate with JavaScript based frameworks such
as Node.js and React.js. This section describes how to install, confifigure and get started using MongoDB.
2.1 Installing and Confifiguring MongoDB
To get started, download MongoDB for free selecting the latest version for your operating system, and click Download.
Run the installer and, if given the choice, choose to run the database as a service so that you don't have to bother having to
restart the database sever every time you login or restart your computer. The MongoDB database will automatically start
whenever you start your computer. On Windows, confifirm the database is running by searching for MongoDB in the
Services dialog. On macOS, confifirm the database is running by
clicking the MongoDB icon in the Systems Settings dialog. The
service dialog gives you controls to start and stop the database, but
it should already be confifigured to start automatically when you
restart your computer.
2.1.1 Installing MongoDB Manually (optional)
On macOS you can install MongoDB using brew by typing the following at the command line
brew install mongodb-atlas
atlas setup
Alternatively you can unzip the MongoDB server from the downloaded archive to a local fifile system and add the right
commands to your operating system PATH environment variable. On macOS, unzip the fifile into /usr/local which creates a
directory such as /usr/local/mongodb-macos-x86_64-5.0.3 (your version might differ). To be able to execute the database
related commands, add the path to the .bash_profifile or .zshrc fifile located in your home directory. Add the following line in
the confifiguration fifile as shown below. Your actual version might differ.
~/.bash_profile or ~/.zshrc
export PATH="$PATH:/usr/local/mongodb-macos-x86_64-5.0.3/bin"
If the .bash_profifile or .zshrc fifile does not exist in your home directory, create it as a plain text fifile, but with no extensions
and a period in front of it. Confifigure it as shown above and then restart your computer.
On Windows, unzip the fifile into C:\Program Files. To confifigure environment variables on Windows press the Windows + R
key combination to open the Run prompt, type sysdm.cpl and press OK. In the System Properties window that appears,
press the Advanced tab and then the Environment Variables button. In the Environment Variables confifiguration window
select the Path variable and press the Edit button. Copy and paste the path of the bin directory in the mongodb directory
you unzipped the MongoDB download, e.g., "C:\Program Files\mongodb-macos-x86_64-5.0.3\bin". The actual path might
differ. Press OK and restart the computer.
2.1.2 Starting MongoDB from the Command Line
If you installed MongoDB as a service, it is already running in the background and can be confifigured and restarted in
Windows from the Services dialog or from the System Settings on macOS. Alternatively you can start the MongoDB server
from the command line using the mongod executable in the bin directory where you installed MongodDB. First you'll needto create a data folder where the server will store all its data. You can create a data folder in your home directory as shown
below.
cd ~
mkdir data
When you start MongoDB, you'll need to tell it where the data folder is with the dbpath option. If you installed MongoDB on
Windows in C:\Program Files\mongodb-macos-x86_64-5.0.3, you can start MongoDB from your home directory as shown
below.
cd ~
C:\Program Files\mongodb-macos-x86_64-5.0.3\bin\mongod --dbpath data
Make sure to include the dbpath data option to tell MongoDB where to fifind the data directory.
If you installed MongoDB on macOS in /usr/local/mongodb-macos-x86_64-6.0.5, you can start MongoDB from your home
directory as shown below.
cd ~
/usr/local/mongodb-macos-x86_64-6.0.5/bin/mongod --dbpath data
2.2 Using MongoDB Compass to Interact with MongoDB
Your installation should have installed MongoDB Compass, a user interface client to the MongoDB database. If not,
MongoDB Compass can be downloaded from MongoDB's download page. You can start Compass from your applications
folder, or search for it in your operating system's search feature. On
macOS bring up Spotlight by pressing the magnifying glass on the top
right menu bar, or press the Command ( ) and Spacebar. Type
MongoDB Compass in the search bar and select the application from
the result list. On Windows press the Window key to bring up the search
fifield, type MongoDB Compass, and select the application from the
result list. When Compass comes up, confifirm that the connection
string mongodb:/ 127.0.0.1:27017 appears in the New Connection
screen, and press Connect to connect to MongoDB.
2.3 Creating a MongoDB Database
Once you are connected to a running MongoDB server, click on Databases on the left side bar and then click the Create
database button on the Databases tab. In the Create Database dialog that appears, name your database kanbas and your
fifirst collection as users. Click Create Database to create the kanbas database.2.4 Inserting and Retrieving Data with Compass
In MongoDB, data is organized into collections, which are analogous to tables in relational databases. Data contained in
collections are referred to as documents, which are analogous to records in relational databases. To create, or insert
documents into a collection in a MongoDB database using Compass, select the database on the left sidebar and then
select the collection you want to insert documents into. For instance, select the kanbas database and then the users
collection as shown below on the right. On the right side, selected Add Data and then Insert Document. In the Insert to
Collection kanbas.users dialog that appears, insert the document as shown below. Click Insert to insert the document.
Confifirm the document inserted as expected.
You can also import entire JSON fifiles containing data. Import the courses.json fifile we used in earlier assignments under
the Database directory of your React project. To import click ADD DATA, then Import JSON or CSV fifile. Navigate to the
location of courses.json, select the fifile and click Import. Confifirm the courses are imported. Also create the following
collections and import the JSON fifiles linked to each of the collection names. Confifirm all collections are imported:
modules.json, assignments.json, users.json
Note the objects stored in the database have a primary key _id automatically added by MongoDB when they were inserted.
MongoDB primary keys are of type ObjectId and are created automatically by the database so your _id values will differ
from the ones shown in this document.
2.5 Interacting with a MongoDB Database with the Command Line (optional)
Compass is a great graphical user interface to the MongoDB database, but there is also value to knowing how to interact
with the database through a command line interface. At the bottom of the Compass window there's a _MONGOSH window
you can expand to type commands to the database. Let's practice a few commands to retrieve data on the command line.
First select the database we want to interact with.
> use kanbas
switched to db kanbas
All the documents in a collection can be retrieved using the fifind() command on a collection as shown below.
> db.courses.find();
Documents in a collection can be retrieved by pattern matching their properties. The example below illustrates how to
retrieve documents by pattern matching their primary key _id, that is, retrieving the document whose _id fifield matches
ObjectId('6370104926906053f1597ce6'). Your ID will likely be different.
> db.courses.find({_id: ObjectId("654e8c73ea7ead465908d1cc")}){
_id: ObjectId("654e8c73ea7ead465908d1cc"),
name: 'Web Development',
number: 'CS4550',
startDate: '2023-01-10',
endDate: '2023-05-15',
department: 'K123',
credits: 4
}
We can also pattern match any of the other fifields individually or combined with other fifields. The following example
retrieves a document from the courses collection whose number property is equal to RS4560.
> db.courses.find({number: 'RS4560'})
{
_id: 'RS102',
name: 'Aerodynamics',
number: 'RS4560',
startDate: '2023-01-10',
endDate: '2023-05-15'
}
Here's another example retrieving courses in the D134 department.
> db.courses.find({department: 'D134'})
{ _id: 'CH101',
name: 'Organic Chemistry', number: 'CH1230',
startDate: '2023-01-10', endDate: '2023-05-15',
department: 'D134', credits: 3
}
{ _id: 'CH102',
name: 'Inorganic Chemistry', number: 'CH1240',
startDate: '2023-01-10', endDate: '2023-05-15',
department: 'D134', credits: 3
}
{ _id: 'CH103',
name: 'Physical Chemistry', number: 'CH1250',
startDate: '2023-01-10', endDate: '2023-05-15',
department: 'D134', credits: 3
}
3 Programming with a MongoDB database
In the previous section we practiced interacting with the MongoDB database through the Compass graphical interface as
well as manually on the command line with MONGOSH. This is all and good to make occasional simple queries to confifirm
the data behaves as expected, but to create applications we're going to need to interact with the database
programmatically with libraries such as Mongoose. The following sections describe how to install, confifigure, and connect
a Node.js application to a MongoDB database server using the Mongoose library. The fifinal section discusses how to
confifigure the application to integrate to a MongoDB database hosted in the Atlas cloud service. Do all your work in a new
GitHub branch called a6 in both your React.js and Node.js projects.
3.1 Installing and Connecting to a MongoDB Database
The Mongoose library provides a set of operations and abstractions that enhance a MongoDB database and leverages the
familiarity of the MONGOSH command line client. To use the Mongoose library, install it from the root of the Node.js
project as shown below.
$ npm install mongooseTo connect to the database server programmatically, import the Mongoose library and then use the connect function as
shown below. The URL in the connect function is called the connection string and is currently referring to a MongoDB
server instance running in the localhost machine (your current laptop or desktop) listening at port 27017 and the kanbas
database existing in that server. In a later section we'll revisit the connection string and confifigure it to connect to a
database server running in a remote machine hosted by Mongo's Atlas cloud service.
index.js
import express from "express";
import mongoose from "mongoose";
...
const CONNECTION_STRING = "mongodb://127.0.0.1:27017/kanbas"
mongoose.connect(CONNECTION_STRING);
const app = express();
...
// load the mongoose library
// connect to the kanbas database
3.2 Confifiguring Connection Strings as Environment Variables
Instead of hard coding the connection string in the source code, it's better to confifigure it as an environment variable and
then reference it from the code. This will come in handy when the server application is deployed to a remote service such
as Render or Heroku and the connection string can be confifigure to reference the online remote database running on Atlas
cloud servive. In a new .env fifile, declare the following connection string environment variable.
.env
MONGO_CONNECTION_STRING=mongodb://127.0.0.1:27017/kanbas
Install the dotenv library to read confifigurations in the local environment.
$ npm install dotenv
Then in index.js, import the dotenv library to read the connection string as shown below.
index.js
import "dotenv/config";
import express from "express";
import mongoose from "mongoose";
...
const CONNECTION_STRING = process.env.MONGO_CONNECTION_STRING || "mongodb://127.0.0.1:27017/kanbas"
mongoose.connect(CONNECTION_STRING);
const app = express();
app.use(cors());
app.use(express.json());
...
app.listen(process.env.PORT || 4000);
3.3 Implementing Mongoose Schemas and Models
Now that we have the collections setup in our database, let's now discuss how to connect and interact with the collections
in the database using the Mongoose library. We'll create Mongoose Schemas and Models so that we can connect and
interact to the database programmatically.
As mentioned earlier, non relational database do not require specifying the structure, or schema of the data stored in
collections like relational databases do. That responsibility has been delegated to the applications using non relational
databases. Mongoose schemas describe the structure of the data being stored in the database and it's used to validate
the data being stored or modifified through the Mongoose library. The schema shown below describes the structure for the
users collection imported earlier. Create the schema in a Users directory in your Node.js projects.Kanbas/Users/schema.js
import mongoose from "mongoose";
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
firstName: String,
email: String,
lastName: String,
dob: Date,
role: {
type: String,
enum: ["STUDENT", "FACULTY", "ADMIN", "USER"],
default: "USER",
},
loginId: String,
section: String,
lastActivity: Date,
totalActivity: String,
},
{ collection: "users" }
);
export default userSchema;
// load the mongoose library
// create the schema
// String field that is required and unique
// String field that in required but not unique
// String fields
// with no additional
// configurations
// Date field with no configurations
// String field
// allowed string values
// default value if not provided
// store data in "users" collection
3.4 Implementing Mongoose Models
In earlier sections we demonstrated using the command line client to interact manually with the MongoDB server using
the fifind command. Mongoose models provide similar functionality to interact with MongoDB programmatically instead of
manually. The functions are similar to the ones found in the mongo shell client: fifind(), create(), updateOne(), removeOne(),
etc. In Users/model.js below, create a Mongoose model from the users schema. The functions provided by Mongoose
models are deliberately generic because they can interact with any collection confifigured in the schema. In the next
section we'll create a data access object that implements higher level functions specifific to the domain of kanbas.
Kanbas/Users/model.js
import mongoose from "mongoose";
import schema from "./schema.js";
const model = mongoose.model("UserModel", schema);
export default model;
// load mongoose library
// load users schema
// create mongoose model from the schema
// export so it can be used elsewhere
3.5 Retrieving data from Mongo with Mongoose
The Mongoose model created in the previous section provides low level functions such as fifind, create, updateOne, and
deleteOne, that are deliberately vague since they need to be able to operate on any collection. It is good practice to wrap
these low level generic functions into higher level functions that are specifific to the use cases of the specifific projects. For
instance instead of just using the generic fifind() function, we'd prefer something such as fifindUsers(), fifindUserById() or
fifindUserByUsername(). A previous assignment implemented a data access object using arrays declared in fifiles. This
assignment refactors the DAOs so they use an actual database. The following Users/dao.js re-implements the CRUD
operations for the users collection written in terms of the low level Mongoose model operations.
Kanbas/Users/dao.js
import model from "./model.js";
import db from "../Database/index.js";
export const createUser = (user) => {} // implemented later
export const findAllUsers = () => model.find();
export const findUserById = (userId) => model.findById(userId);
export const findUserByUsername = (username) => model.findOne({ username: username });
export const findUserByCredentials = (username, password) => model.findOne({ username, password });
export const updateUser = (userId, user) => model.updateOne({ _id: userId }, { $set: user });
export const deleteUser = (userId) => model.deleteOne({ _id: userId });3.6 Implementing APIs to interact with MongoDB from a React client application
DAOs implement an interface between an application and the low level database access, providing a high level API to the
rest of the application hiding the details and idiosyncrasies of using a particular database vendor. Likewise routes
implement an interface between the HTTP network world and the JavaScript object and function world by converting a
stream of bits from a network connection request into a set of objects, maps, and function event handlers that participate
in the client/server architecture of a multi tiered application.
3.6.1 Refactoring Account Routes
Previous assignments implemented account routes such as signin and signup shown below. Since the DAO
implementations used data structures imported from the local fifile system, the operations were synchronous. Now that the
DAO is interacting with a database, the operations are asynchronous and must be tagged with the async/await keywords
as shown below. Confifirm Signin, Signup, and Profifile screens work as before. Following the examples below for the signin
and signup functions, add the async keyword to all other router functions and add the await keyword to all calls to dao
functions.
Kanbas/Users/routes.js
import * as dao from "./dao.js";
export default function UserRoutes(app) {
const signin = async (req, res) => {
const { username, password } = req.body;
const currentUser = await dao.findUserByCredentials(username, password);
if (currentUser) {
req.session["currentUser"] = currentUser;
res.json(currentUser);
} else {
res.status(401).json({ message: "Unable to login. Try again later." });
}
};
const signup = async (req, res) => {
const user = await dao.findUserByUsername(req.body.username);
if (user) {
res.status(400).json({ message: "Username already taken" });
return;
}
const currentUser = await dao.createUser(req.body);
req.session["currentUser"] = currentUser;
res.json(currentUser);
};
...
app.post("/api/users/signin", signin);
app.post("/api/users/signup", signup);
}
3.6.2 Retrieving All Documents from MongoDB with Mongoose
DAOs implement high level data operations based on lower level Mongoose models. The Mongoose model fifind function
retrieves all documents from a collection. The fifindAllUsers function below uses fifind to retrieve all the users from the
users collection.
Kanbas/Users/dao.js
import model from "./model.js";
export const findAllUsers = () => model.find();
Routes implement RESTful Web APIs that user interface clients can use to interact with server functionality. The route
implemented below uses the fifindAll function implemented by the DAO to retrieve all the users from the database. The
route responds with the collection of users retrieved from the database. Confifirm the route works by navigating to
http:/ localhost:4000/api/users with your browser.Kanbas/Users/routes.js
import * as dao from "./dao.js";
let currentUser = null;
export default function UserRoutes(app) {
const findAllUsers = async (req, res) => {
const users = await dao.findAllUsers();
res.json(users);
};
app.get("/api/users", findAllUsers);
...
}
Meanwhile in the React user interface application, in src/Kanbas/Account/ client.ts, implement the fifindAllUsers function
shown below to send a GET request request to the server and await for the server's response containing an array of users
in the data property.
src/Kanbas/Account/client.ts
import axios from "axios";
const axiosWithCredentials = axios.create({ withCredentials: true });
export const REMOTE_SERVER = process.env.REACT_APP_REMOTE_SERVER;
export const USERS_API = `${REMOTE_SERVER}/api/users`;
export const findAllUsers = async () => {
const response = await axiosWithCredentials.get(USERS_API);
return response.data;
};
...
To display the array of users from the database, refactor the PeopleTable component to accept an optional users
parameter, instead of retrieving the users from the local fifile system. Remove any fifilters since it's best done at the server.
src/Courses/People/Table.tsx
import React, { useState, useEffect } from "react";
// import * as db from "../../Database";
// import { useParams } from "react-router-dom";
export default function PeopleTable({ users = [] }: { users?: any[] }) {
// const { cid } = useParams();
// const { users, enrollments } = db;
return (
<div id="wd-people-table">
<table className="table table-striped">
...
<tbody>
{users
.filter((usr) => enrollments.some((enrollment) =>
enrollment.user === usr._id && enrollment.course === cid))
.map((user: any) => ( ... ))}
</tbody>
</table>
</div>);}
Create a new Users screen that fetches the users from the database and displays it with the PeopleTable component as
shown below.
src/Kanbas/Account/Users.tsx
import { useState, useEffect } from "react";
import { useParams } from "react-router";
import PeopleTable from "../Courses/People/Table";
import * as client from "./client";
export default function Users() {
const [users, setUsers] = useState<any[]>([]);
const { uid } = useParams();
const fetchUsers = async () => {
const users = await client.findAllUsers();setUsers(users);
};
useEffect(() => {
fetchUsers();
}, [uid]);
return (
<div>
<h3>Users</h3>
<PeopleTable users={users} />
</div>
);}
Add a new Users route to the Account screen as shown below.
src/Kanbas/Account/index.tsx
...
<Routes>
<Route path="/" element={ <Navigate to={currentUser ? "/Kanbas/Account/Profile" : "/Kanbas/Account/Signin" } /> } />
<Route path="/Signin" element={<Signin />} />
<Route path="/Profile" element={<Profile />} />
<Route path="/Signup" element={<Signup />} />
<Route path="/Users" element={<Users />} />
</Routes>
...
Add a Users link to the Accounts Navigation sidebar that navigates to the Users screen only if the logged in user has an
ADMIN role. To test, use Compass to update the role of an existing user or create a new user with ADMIN role, signin as
the ADMIN user, and navigate to the Users screen. Confifirm that all users are displayed.
src/Account/Navigation.tsx
import { Link } from "react-router-dom";
import { useSelector } from "react-redux";
export default function AccountNavigation() {
const { currentUser } = useSelector((state: any) => state.accountReducer);
const links = currentUser ? ["Profile"] : ["Signin", "Signup"];
const active = (path: string) => (pathname.includes(path) ? "active" : "");
const { pathname } = useLocation();
return (
<div id="wd-account-navigation" className="list-group">
{links.map((link) => (
<Link key={link} to={`/Kanbas/Account/${link}`} className={`list-group-item ${active(link)}`}> {link} </Link>
))}
{currentUser && currentUser.role === "ADMIN" && (
<Link to={`/Kanbas/Account/Users`} className={`list-group-item ${active("Users")}`}> Users </Link> )}
</div>
);}
Logged in as an ADMIN, navigate to the new Users screen and confifirm it displays all the users as shown below.3.6.3 Retrieving Documents by Predicate from MongoDB with Mongoose
In the User's DAO, implement fifindUsersByRole that fifilters the users collection by the role property as shown below.
Mongoose model's fifind function takes as argument a JSON object used to pattern match documents in the collection.
The {role: role} object means that documents will be fifiltered by their role property that matches the value role.
Kanbas/Users/dao.js
export const findUsersByRole = (role) => model.find({ role: role }); // or just model.find({ role })
In the User routes, refactor the fifindAllUsers function so that it parses the role from the query string, and then uses the
DAO to retrieve users with that particular role.
Kanbas/Users/routes.js
const findAllUsers = async (req, res) => {
const { role } = req.query;
if (role) {
const users = await dao.findUsersByRole(role);
res.json(users);
return;
}
const users = await dao.findAllUsers();
res.json(users);
};
In the React user interface application, add fifindUserByRole in the client so that it encodes the role in the query string of
the URL as shown below.
src/Kanbas/Account/client.ts
export const findUsersByRole = async (role: string) => {
const response = await
axios.get(`${USERS_API}?role=${role}`);
return response.data;
};
In the Users screen, add a dropdown that invokes a fifilterUsers event handler function with the selected role. The function
updates a role state variable and requests from the server the list of users fifiltered by their role. Confifirm that selecting
various roles actually fifilters the users by their role.
src/Kanbas/Account/Users.tsx
export default function Users() {
const [users, setUsers] = useState<any[]>([]);
const [role, setRole] = useState("");
const filterUsersByRole = async (role: string) => {
setRole(role);
if (role) {
const users = await client.findUsersByRole(role);
setUsers(users);
} else {
fetchUsers();
}
};
const fetchUsers = async () => { ... };
useEffect(() => { ... }, []);
return (
<div>
<h3>Users</h3>
<select value={role} onChange={(e) =>filterUsersByRole(e.target.value)}
className="form-select float-start w-25 wd-select-role" >
<option value="">All Roles</option> <option value="STUDENT">Students</option>
<option value="TA">Assistants</option> <option value="FACULTY">Faculty</option>
<option value="ADMIN">Administrators</option></select>
...
</div>
);}
Now practice fifiltering users by their fifirst or lastName by creating a regular expression used to pattern match the fifirstName
or lastName fifields of the documents in the users collection.
Kanbas/Users/dao.js
export const findUsersByPartialName = (partialName) => {
const regex = new RegExp(partialName, "i"); // 'i' makes it case-insensitive
return model.find({
$or: [{ firstName: { $regex: regex } }, { lastName: { $regex: regex } }],
});
};
In the the User routes, parse a name parameter from the query string and use it to fifind users whose fifirst or last names
partially match the name parameter.
Kanbas/Users/routes.js
const findAllUsers = async (req, res) => {
const { role, name } = req.query;
if (role) { ... }
if (name) {
const users = await dao.findUsersByPartialName(name);
res.json(users);
return;
}
const users = await dao.findAllUsers();
res.json(users);
};
In the React client application, implement the fifindUserByPartialName client function as shown below which encodes a
name in the query string the server can use to fifilter users by their fifirst and lastName.
src/Kanbas/Account/client.ts
export const findUsersByPartialName = async (name: string) => {
const response = await axios.get(`${USERS_API}?name=${name}`);
return response.data;
};
In the Users screen, create a new name state variable and corresponding input fifield used to invoke the
fifindUsersByPartialName client function and update the users state variable with a subset of users that match the name.
Confifirm that typing a name in the input fifield actually fifilters the users by their fifirst or last name. Note that the current
implementation does not consider a combination of fifiltering by role and by name. Feel free to explore how you would go
about fifiltering by both.
src/Kanbas/Account/Users.tsx
export default function Users() {
const [users, setUsers] = useState<any[]>([]);
const [role, setRole] = useState("");
const [name, setName] = useState("");
const filterUsersByName = async (name: string) => {
setName(name);
if (name) {
const users = await client.findUsersByPartialName(name);
setUsers(users);
} else {
fetchUsers();
}};
...
return (
<div>
<h3>Users</h3>
<input onChange={(e) => filterUsersByName(e.target.value)} placeholder="Search people"
className="form-control float-start w-25 me-2 wd-filter-by-name" />
...
</div>
);
}
3.6.4 Retrieving Documents by Primary Key from MongoDB with Mongoose
A common database operation is to retrieve documents by their primary key. The DAO function below retrieves a user
document by its primary key.
Kanbas/Users/dao.js
export const findUserById = (userId) => model.findById(userId);
Make the fifindUserById DAO function available as a RESTful Web API as shown below.
Kanbas/Users/routes.js
const findUserById = async (req, res) => {
const user = await dao.findUserById(req.params.userId);
res.json(user);
};
app.get("/api/users/:userId", findUserById);
The user interface can then interact with the server using the fifindUserById client function shown below.
src/Kanbas/Account/client.ts
export const findUserById = async (id: string) => {
const response = await axios.get(`${USERS_API}/${id}`);
return response.data;
};
In a new PeopleDetails component, use the client's fifindUserById function to retrieve the user when a faculty clicks on the
user's name. Parse a uid path parameter and use it to retrieve the user by their ID when the component loads. If the uid
does not exist, return null so that the component does not render on the screen. In useEffect, add uid as a dependency so
that if the component re-renders if you click on another user while the component is still displaying.
src/Kanbas/Courses/People/Details.tsx
import { useEffect, useState } from "react";
import { FaUserCircle } from "react-icons/fa";
import { IoCloseSharp } from "react-icons/io5";
import { useParams, useNavigate } from "react-router";
import { Link } from "react-router-dom";
import * as client from "../../Account/client";
export default function PeopleDetails() {
const { uid} = useParams();
const [user, setUser] = useState<any>({});
const navigate = useNavigate();
const fetchUser = async () => {
if (!uid) return;
const user = await client.findUserById(uid);
setUser(user);
};
useEffect(() => {
if (uid) fetchUser();}, [uid]);
if (!uid) return null;
return (
<div className="wd-people-details position-fixed top-0 end-0 bottom-0 bg-white p-4 shadow w-25">
<button onClick={() => navigate(-1)} className="btn position-fixed end-0 top-0 wd-close-details">
<IoCloseSharp className="fs-1" /> </button>
<div className="text-center mt-2"> <FaUserCircle className="text-secondary me-2 fs-1" /> </div><hr />
<div className="text-danger fs-4 wd-name"> </div>
<b>Roles:</b> <span className="wd-roles"> </span> <br />
<b>Login ID:</b> <span className="wd-login-id"> </span> <br />
<b>Section:</b> <span className="wd-section"> </span> <br />
<b>Total Activity:</b> <span className="wd-total-activity"></span> </div> ); }
Add a close button rendered as an X at the top left that hides the component by navigating back to the Users screen as
shown above. Confifirm that clicking on the the name of a user displays the user's details. Also confifirm that closing the
PeopleDetails hides the component.
src/Kanbas/Courses/People/Table.tsx
...
import * as client from "../../Account/client";
import PeopleDetails from "./Details";
export default function PeopleTable() {
...
return (
<div id="wd-people-table">
<PeopleDetails />
...
{users
.map((user) => (
<tr key=>
<td className="wd-full-name text-nowrap">
<Link to={`/Kanbas/Account/Users/$`} className="text-decoration-none">
<FaUserCircle className="me-2 fs-1 text-secondary" />
<span className="wd-first-name"></span>{" "}
<span className="wd-last-name"></span>
</Link>
</td>
...
</tr>
))}
...
</div>
);}
In the Account screen, add a Route that encodes the uid path parameter in the URL that renders the same Users screen.
src/Kanbas/Account/index.tsx
...
<Routes>
<Route path="/" element={ <Navigate to={currentUser ? "/Kanbas/Account/Profile" : "/Kanbas/Account/Signin" } /> } />
<Route path="/Signin" element={<Signin />} />
<Route path="/Profile" element={<Profile />} />
<Route path="/Signup" element={<Signup />} />
<Route path="/Users" element={<Users />} />
<Route path="/Users/:uid" element={<Users />} />
</Routes>
...
3.6.5 Deleting a Document in MongoDB with Mongoose
To delete user documents from the users MongoDB collection, implement the deleteUser operation as shown below. The
DAO function below removes a single user document from the database based on its primary key.
Kanbas/Users/dao.jsexport const deleteUser = (userId) => model.deleteOne({ _id: userId });
The route below makes the deleteUser operation available as a RESTful Web API for integration with the user interface
which encodes the id of the user to remove as a path parameter.
Kanbas/Users/routes.js
const deleteUser = async (req, res) => {
const status = await dao.deleteUser(req.params.userId);
res.json(status);
};
app.delete("/api/users/:userId", deleteUser);
In the React Web App, implement client function that integrates with the deleteUser route in the server.
src/Kanbas/Account/client.ts
export const deleteUser = async (userId: string) => {
const response = await axios.delete( `${USERS_API}/${userId}` );
return response.data;
};
...
In the PeopleDetails component add buttons Cancel and Delete as shown below. The Delete button invokes a new
deleteUser event handler function with uid, the ID of the user to delete. Pass a reference to fetchUsers as a parameter so
that PeopleDetails can notify PeopleTable that a user has been remove and
that the list of users must be updated.
src/Kanbas/Courses/People/Details.tsx
...
import { useNavigate, useParams } from "react-router";
export default function PeopleDetails() {
const navigate = useNavigate();
const deleteUser = async (uid: string) => {
await client.deleteUser(uid);
navigate(-1);
};
...
return (
<div className="...">
...
<hr />
<button onClick={() => deleteUser(uid)} className="btn btn-danger float-end wd-delete" > Delete </button>
<button onClick={() => navigate(-1)}
className="btn btn-secondary float-start float-end me-2 wd-cancel" > Cancel </button>
</div>
);}
Use the client's deleteUser to remove the user, and navigate back to hide the details component. The Cancel button just
hides the details without removing any documents. Confifirm that clicking the Cancel and Delete buttons actually work.
3.6.6 Updating a Document in MongoDB with Mongoose
The Mongoose update function updates documents in MongoDB databases. In the User's DAO, implement updateUser as
shown below to update a single document by fifirst identifying it by its primary key, and then updating the matching fifields in
the user parameter.
Kanbas/Users/dao.js
import model from "./model.js";
export const updateUser = (userId, user) => model.updateOne({ _id: userId }, { $set: user });...
In the User's routes, make the DAO function available as a RESTful Web API as shown below. Map a route that accepts a
user's primary key as a path parameter, passes the ID and request body to the DAO function and responds with the status.
Kanbas/Users/routes.js
export default function UserRoutes(app) {
...
const updateUser = async (req, res) => {
const { userId } = req.params;
const userUpdates = req.body;
await dao.updateUser(userId, userUpdates);
const currentUser = req.session["currentUser"];
if (currentUser && currentUser._id === userId) {
req.session["currentUser"] = { ...currentUser, ...userUpdates };
}
res.json(currentUser);
};
app.put("/api/users/:userId", updateUser);
...
}
In the React client application, the client function updateUser sends user updates to the server to be saved to the
database.
src/Kanbas/Account/client.ts
...
export const updateUser = async (user: any) => {
const response = await axiosWithCredentials.put(`${USERS_API}/$`, user);
return response.data;
};
...
In the PeopleDetails component, add a name state variable to edit the fifirst and last name of the user. Also add a editing
state variable to toggle the input fifield that edits the name. With the navigate routing function, navigate back to
PeopleTable once the update is done. Create a new saveUser function that splits the name state variable into the
fifirstName and lastName and sends an updated version of the user to the server. Also update the local user state variable,
turn off editing, and navigate back to the PeopleTable.
src/Kanbas/Courses/People/Details.tsx
...
import { FaPencil } from "react-icons/fa6";
import { FaCheck, FaUserCircle } from "react-icons/fa";
export default function PeopleDetails() {
...
const [name, setName] = useState("");
const [editing, setEditing] = useState(false);
const saveUser = async () => {
const [firstName, lastName] = name.split(" ");
const updatedUser = { ...user, firstName, lastName };
await client.updateUser(updatedUser);
setUser(updatedUser);
setEditing(false);
navigate(-1);
};
...
return (
...
<div className="text-danger fs-4">
{!editing && (
<FaPencil onClick={() => setEditing(true)}
className="float-end fs-5 mt-2 wd-edit" /> )}
{editing && (
// to edit the user's first and last name
// whether we are editing or not
// to save updates to user's name
// split the name into an array and get first
// and last and create new version of user overriding
// first and last. Send update to server
// update local copy of the user
// we're done editing
// go back to PeopleTable
// if not editing show pencil icon
// clicking pencil turns on editing and hides pencil<FaCheck onClick={() => saveUser()}
className="float-end fs-5 mt-2 me-2 wd-save" /> )}
{!editing && (
<div className="wd-name"
onClick={() => setEditing(true)}>
</div>)}
{user && editing && (
<input className="form-control w-50 wd-edit-name"
defaultValue={`$ $`}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") { saveUser(); }}}/>)}
</div>
...
);}
// if editing show check mark. Clicking check turns
// off editing, saves and hides check
// if not editing show first and last name
// clicking on name turns on editing
// if editing show input field to edit name
// name is initially concatenation of first and last
// update name as we type
// save the user if Enter key is pressed
Add pencil and check icons to turn editing on and off. Hide each icon based on the editing boolean state variable. Clicking
the name of the user also turns editing on. If editing is on, hide the user's name and instead display an input fifield that
shows the current user's fifirstName and lastName and edits the name state variable. Pressing the Enter key saves the
updated user's details. Confifirm users can be edited. On your own, add the ability to edit a user's email and role. Use an
input fifield of type email to edit the email. To edit the user's role, use a dropdown similar to the one used to fifilter users by
their role.
3.6.7 Creating New Documents in MongoDB with Mongoose
In the User's DAO, implement the createUser function as shown below to insert a new user object into the users collection.
Kanbas/Users/dao.js
export const createUser = (user) => {
delete user._id
return model.create(user);
}
// remove _id field just in case client sends it
// database will create _id for us instead
Make sure the incoming user object does not have an _id property since it can interfere with the database insert operation.
In the User's routes, make the DAO operation available as a RESTful Web API for the user interface to interact with. The
new user is posted to the route in the request's body. The DAO createUser function inserts the new user into the database
and returns the newly inserted user which is sent back to the user interface in the response.
Kanbas/Users/routes.js
import * as dao from "./dao.js";
let currentUser = null;
export default function UserRoutes(app) {
...
const createUser = async (req, res) => {
const user = await dao.createUser(req.body);
res.json(user);
};
app.post("/api/users", createUser);
...
}
In the React client application, implement a createUser client function to interact with the route created above. Post the
new user object to the server as shown below.
src/Kanbas/Account/client.ts
export const createUser = async (user: any) => {
const response = await axios.post(`${USERS_API}`, user);
return response.data;
};In the Users screen, implement a new createUser event handler that sends a new user object to be inserted in the
database. Use default values for the fifields as shown and confifirm that clicking the new + People button actually creates
the new user. Optionally implement editing those fifields in the PeopleDetails component.
src/Kanbas/Account/Users.tsx
export default function Users() {
const { uid } = useParams();
const [users, setUsers] = useState<any[]>([]);
const [role, setRole] = useState("");
const [name, setName] = useState("");
const createUser = async () => {
const user = await client.createUser({
firstName: "New",
lastName: `User${users.length + 1}`,
username: `newuser${Date.now()}`,
password: "password123",
email: `email${users.length + 1}@neu.edu`,
section: "S101",
role: "STUDENT",
});
setUsers([...users, user]);
};
return (
<div>
<button onClick={createUser} className="float-end btn btn-danger wd-add-people">
<FaPlus className="me-2" />
Users
</button>
...
</div>
);}
4 Integrating with MongoDB Hosted in Atlas Cloud Service
When you run your server on your development environment, it should be
connecting to a MongoDB instance running on the same local development
computer. When you deploy the server on a remote server such as Render, Heroku
or AWS, the server needs to connect to a database that is also hosted on a public
site. MongoDB Atlas Cloud Service provides a hosted database service where a
MongoDB instances run on public servers, and they provide a connection string to
integrate our Node.js application. This section describes setting up and deploying
the database online and then integrating with it from our Node.js server running
on Heroku.
4.1 Setting up MongoDB Atlas
To get started, head over to https://www.mongodb.com/ and click on Sign in at the top
right corner. Login with your Google account or click on Sign Up to create an account
with an email and password. If you get a validation email, confifirm it and login. Answer
any general questions if asked during the sign up process. In the Deploy your cluster
screen choose a Free plan for now which should be enough for this course. Name your
cluster Kanbas. In the Provider section, choose any of the cloud providers and in the
Region section choose a region close to your geographic area, for instance AWS and
North Virginia, and then click Create Deployment. In the Connect to Kabas screen, in
the Create a database user section create credentials to login to your database. In the
Username and Password fifields, type credentials you'll remember later since these are
the credentials mongoose will use to login to the database from your Node.js server
application when running on Render or Heroku. If you forget these credentials you'llneed to create new ones later. I went with giuseppi and supersecretpassword :) and clicked Create Database User.
4.1.1 Connecting to a Remote Database from Compass
Then click the Choose a connection method button, and in the Access your data through tools, select Compass. In the
Connecting with MongoDB Compass screen, in the Copy the connection string, then open MongoDB Compass section,
copy the connection string which should look something like so
mongodb+srv://giuseppi:[email protected]/
Click Done. From Compass select Connect, New Window, paste the connection string in the URI fifield, and then click
connect. Following the same steps used earlier, import the courses, modules, and users into the remote database.
4.1.2 Connecting from Node.js
In the Atlas main window click on Network Access and in the Network Access
screen IP Access List tab, select + ADD IP ADDRESS. In the Add IP Access List
Entry dialog click on ALLOW ACCESS FROM ANYWHERE. This will add 0.0.0.0/0 to
the Access List Entry, allowing any computer to connect. Click Confifirm and verify
the new entry appears in the Network Access screen. Click Database on the left
and in the Clusters screen, click Connect. In the Connect to Kanbas dialog, in the
Connect to your application, select Drivers and version section, confifirm the Driver
is set to Node.js and Version is set to 5.5 or later. In the Add your connection
string into your application code section, copy the the URL. It should look similar to the following.
Node MongoDB Database Connection String
mongodb+srv://giuseppi:<password>@kanbas.jxui0bc.mongodb.net/kanbas?retryWrites=true&w=majority&appName=Kanbas
Commit and push your code to a branch called a6 and
deploy the server application to a new remote service
running on Render, Heroku, or AWS. Make sure not to
deploy to the server for prior assignments since TAs might
still be grading it. In the new remote server, confifigure an
environment variable called MONGO_CONNECTION
_STRING with the URL value above. For instance, in the
Render dashboard click on Environment, type
MONGO_CONNECTION_STRING in the Key fifield and the
URL in the Value fifield. Replace the <password> with the actual password created in an earlier step. Commit and deploy the
React application to a new a6 branch. Confifigure the REACT_APP_REMOTE_SERVER to point to the new remote server. For
the environment variables to take effect, you might need to redeploy and/or restart the remote Node server on Render as
well as the remote React application server on Netlify.
4.2 Confifiguring Session in Remote Servers
The local Node server was confifigured to support multiple
sessions. Similarly, the remote server also needs to be
confifigured to support sessions. In Render.com, navigate to
the Environment section of the application dashboard. Click
Add Environment Variable, type the name of the variables in
the Key column and the variable's value in the Value column.
Repeat for each of the environment variables as shown hereon the right. You will need to restart the remote server by clicking Manual Deploy and then Deploy latest commit. Below is
an example of the environment variables used to confifigure a remote server running on Render.com. Use the same
Environment Variable keys shown on the left column below. Don't use the values shown on the right column below.
Instead use the values for your Mongo Database, your Netlify remote server, and your remote Node server.
Environment Variable Value
MONGO_CONNECTION_STRING mongodb+srv://giuseppi:[email protected]/kanbas?retryWrit
es=true&w=majority&appName=Kanbas
NETLIFY_URL https://a6--kanbas-react-web-app-su24.netlify.app
NODE_SERVER_DOMAIN kanbas-node-server-app-su24.onrender.com
NODE_ENV production
SESSION_SECRET what ever
5 Implementing the Kanbas MongoDB
For the last several assignments we have been implementing the Kanbas application rendering various courses, modules
and assignments. We used JSON fifiles to represent the data for the application, and wrapped the data structures into a
"Database" that fifirst lived in the React application and then in the Node server. It is time for the data to live where it
belongs. In this section, on your own, migrate the courses and modules into corresponding collections in the kanbas
MongoDB database. Create the Mongoose schemas, models and DAOs, and refactor the RESTful Web APIs to CRUD
courses and modules in the database. Confifirm that all courses and modules CRUD functionality works as expected.
Optionally also migrate the assignments into a collection and confifirm that all assignment CRUD operations work. Note
that the modules course fifield might be referencing an ID of a course that does not exist. Update the module course fifields
so that they refer to the correct course document.
5.1 Persisting Courses in the Database
The current server application implements RESTful Web APIs to access courses stored in a fifile courses.js. This section
replaces the source of the courses in a MongoDB courses collection. The basic operations on any data source are create,
read, update and delete, colloquially referred to as CRUD operations. The following sections demonstrate how to
implement several CRUD operations.
5.1.1 Retrieving Courses from a Database
To access the courses collection created earlier, create a mongoose schema fifile as shown below. The schema fifile
describes the constraints of the documents stored in a collection, such as the names of the properties, their data types,
and the name of the collection where the documents will be stored.
Kanbas/Courses/schema.js
import mongoose from "mongoose";
const courseSchema = new mongoose.Schema(
{
name: String,
number: String,
credits: Number,
description: String,
},
{ collection: "courses" }
);
export default courseSchema;Using the mongoose schema fifile, create a mongoose model fifile as shown below. Mongoose models provide functions to
interact with the collection such as fifind(), create(), updateOne(), and deleteOne(). The CourseModel in the mongoose
model declares a unique name that can be used as a reference from other mongoose schemas.
Kanbas/Courses/model.js
import mongoose from "mongoose";
import schema from "./schema.js";
const model = mongoose.model("CourseModel", schema);
export default model;
Using the mongoose model, create a DAO fifile that implements various CRUD operations to interact with the courses
collection. Start by refactoring the fifindAllCourses() function to retrieve all the courses from the database instead of the
Database fifile as shown below. The model.fifind() function returns an array containing all the courses documents in the
courses collection.
Kanbas/Courses/dao.js
// import Database from "../Database/index.js";
import model from "./model.js";
export function findAllCourses() {
// return Database.courses;
return model.find();
}
...
The functions in mongoose models all return promises allowing asynchronous communication with the MongoDB server.
In the Courses routes fifile, declare all router functions as asynchronous by adding the async keyword in front of the router
functions as shown below. Also add the await keyword in front of all asynchronous calls of the DAO functions such as
dao.fifindAllCourses() as shown below.
Kanbas/Courses/routes.js
import * as dao from "./dao.js";
export default function CourseRoutes(app) {
app.get("/api/courses", async (req, res) => {
const courses = await dao.findAllCourses();
res.send(courses);
});
...
}
In the React.js project, refactor the fetchAllCourses client function so that it uses a version of axios that includes cookies
in the request, as shown below.
src/Kanbas/Courses/client.ts
import axios from "axios";
const axiosWithCredentials = axios.create({ withCredentials: true });
const REMOTE_SERVER = process.env.REACT_APP_REMOTE_SERVER;
const COURSES_API = `${REMOTE_SERVER}/api/courses`;
export const fetchAllCourses = async () => {
const { data } = await axiosWithCredentials.get(COURSES_API);
return data;
};
In the Kanbas root component, import course client and refactor the fetchCourse function to fetch all the courses, as
shown below.src/Kanbas/index.tsx
import { useEffect, useState } from "react";
import * as courseClient from "./Courses/client";
export default function Kanbas() {
const { currentUser } = useSelector((state: any) => state.accountReducer);
const [courses, setCourses] = useState<any[]>([]);
const fetchCourses = async () => {
try {
const courses = await courseClient.fetchAllCourses();
setCourses(courses);
} catch (error) {
console.error(error);
}
};
useEffect(() => {
fetchCourses();
}, [currentUser]);
return ( ... );
}
In the Dashboard screen, remove the fifilter since fifiltering is best implemented on the server. Start the database, server, and
client application, sign in on the React application, and navigate to the Dashboard screen. Confifirm that all courses are
rendered.
src/Kanbas/Dashboard.tsx
export default function Dashboard({ ... }) {
return (
<div id="wd-dashboard">
<h1 id="wd-dashboard-title">Dashboard</h1> <hr />
<div id="wd-dashboard-courses" className="row">
<div className="row row-cols-1 row-cols-md-5 g-4">
{courses
.filter((course) => ( ... ))
.map((course) => ( ... ))}
</div>
</div>
);}
5.1.2 Inserting Courses into a Database
Refactor the DAO's createCourse() function to insert new courses into the database with the mongoose model as shown
below.
Kanbas/Courses/dao.js
import model from "./model.js";
export function createCourse(course) {
delete course._id;
return model.create(course);
// const newCourse = { ...course, _id: Date.now().toString() };
// Database.courses = [...Database.courses, newCourse];
// return newCourse;
}
...
Refactor the Courses routes fifile by adding keywords async and await before the route and DAO functions as shown below.
Kanbas/Courses/routes.js
import * as dao from "./dao.js";
export default function CourseRoutes(app) {
app.post("/api/courses", async (req, res) => {
const course = await dao.createCourse(req.body);
res.json(course);});
...
}
In the React.js Courses client fifile, refactor the createCourse client function so that it uses the axios instance with
credentials as showns below.
Kanbas/Courses/client.js
import axios from "axios";
const axiosWithCredentials = axios.create({ withCredentials: true });
const REMOTE_SERVER = process.env.REACT_APP_REMOTE_SERVER;
const COURSES_API = `${REMOTE_SERVER}/api/courses`;
export const createCourse = async (course: any) => {
const { data } = await axiosWithCredentials.post(COURSES_API, course);
return data;
};
Refactor the Kanbas root component so that it uses the createCourse client function in the Course client fifile as shown
below. Confifirm that new courses are inserted in the courses collection in the database.
Kanbas/index.js
import { useEffect, useState } from "react";
import * as courseClient from "./Courses/client";
export default function Kanbas() {
const { currentUser } = useSelector((state: any) => state.accountReducer);
const [course, setCourse] = useState<any>({ ... });
const addNewCourse = async () => {
// const newCourse = await userClient.createCourse(course);
const newCourse = await courseClient.createCourse(course);
setCourses([...courses, newCourse]);
};
...
return ( ... );
}
5.1.3 Deleting Courses from the Database
Refactor the deleteCourse() DAO function to delete courses from the database by using the courses model as shown
below.
Kanbas/Courses/dao.js
export function deleteCourse(courseId) {
return model.deleteOne({ _id: courseId });
}
...
In the Courses routes fifile, refactor the route that deletes courses by adding keywords async and await in front of the route
and DAO functions as shown below.
Kanbas/Courses/routes.js
import * as dao from "./dao.js";
export default function CourseRoutes(app) {
app.delete("/api/courses/:courseId", async (req, res) => {
const { courseId } = req.params;
const status = await dao.deleteCourse(courseId);
res.send(status);
});
...
}In the React.js application, refactor the deleteCourse() client function so that it uses the axios instance with credentials.
src/Kanbas/Courses/client.ts
import axios from "axios";
const axiosWithCredentials = axios.create({ withCredentials: true });
const REMOTE_SERVER = process.env.REACT_APP_REMOTE_SERVER;
const COURSES_API = `${REMOTE_SERVER}/api/courses`;
export const deleteCourse = async (id: string) => {
const { data } = await axiosWithCredentials.delete(`${COURSES_API}/${id}`);
return data;
};
...
Refactor the Kanbas root component by adding keywords async and await in front of the deleteCourse event handler
function and Course client function as shown below. Confifirm that courses are removed from the courses collection.
src/Kanbas/index.ts
import { useSelector } from "react-redux";
import { useEffect, useState } from "react";
import * as courseClient from "./Courses/client";
export default function Kanbas() {
const { currentUser } = useSelector((state: any) => state.accountReducer);
const [courses, setCourses] = useState<any[]>([]);
const deleteCourse = async (courseId: string) => {
const status = await courseClient.deleteCourse(courseId);
setCourses(courses.filter((course) => course._id !== courseId));
};
...
5.1.4 Updating Courses in the Database
In the Courses DAO, refactor the updateCourses function to update courses in the database with the model.updateOne()
function as shown below.
Kanbas/Courses/dao.js
import model from "./model.js";
export function updateCourse(courseId, courseUpdates) {
return model.updateOne({ _id: courseId }, { $set: courseUpdates });
// const { courses } = Database;
// const course = courses.find((course) => course._id === courseId);
// Object.assign(course, courseUpdates);
// return course;
}
In the Courses routes, refactor the routing function by adding async and await keywords before the routing and DAO
function calls as shown below.
Kanbas/Courses/routes.js
import * as dao from "./dao.js";
export default function CourseRoutes(app) {
app.put("/api/courses/:courseId", async (req, res) => {
const { courseId } = req.params;
const courseUpdates = req.body;
const status = await dao.updateCourse(courseId, courseUpdates);
res.send(status);
});
...
}In the Courses client fifile, refactor the client function to use axios with credentials as shown below. Confifirm that updating
the title of a course is persisted in the database.
src/Kanbas/Courses/client.ts
import axios from "axios";
const axiosWithCredentials = axios.create({ withCredentials: true });
const REMOTE_SERVER = process.env.REACT_APP_REMOTE_SERVER;
const COURSES_API = `${REMOTE_SERVER}/api/courses`;
export const updateCourse = async (course: any) => {
const { data } = await axiosWithCredentials.put(`${COURSES_API}/${course._id}`, course);
return data;
};
5.2 Persisting Modules in a Database as One to Many Relations with Courses
In Kanbas, each course contains several modules, establishing a one to many relationship. The relationship is
implemented by each module having a fifield that refers to the course they below to. This section demonstrates how to use
Mongoose to implement one to many relationships.
5.2.1 Declaring One to Many Relationships
In Mongoose, relationships are implemented by declaring fifields of type ObjectId and providing the name of the model that
is related to. To demonstrate, create the schema fifile below that describes the data structure of module documents stored
in a collection called modules. The course fifield is declared as type mongoose.Schema.Types.ObjectId to store a primary
key value. The ref property set to the value CourseModel is the same value as the name of the courses model stored in the
courses collection, declared in an earlier section. The ref property establishes that the primary key stored in course refers
to a document stored in the courses collection, effectively implementing a one to many relation.
Kanbas/Modules/schema.js
import mongoose from "mongoose";
const schema = new mongoose.Schema(
{
name: String,
description: String,
course: { type: mongoose.Schema.Types.ObjectId, ref: "CourseModel" },
},
{ collection: "modules" }
);
export default schema;
In a Modules model fifile implement a mongoose model to interact with the module collection in the database. The model
implements various functions to implement CRUD operations such as fifind(), create(), deleteOne(), updateOne().
Kanbas/Modules/model.js
import mongoose from "mongoose";
import schema from "./schema.js";
const model = mongoose.model("ModuleModel", schema);
export default model;
5.2.2 Creating Modules for a Course
Refactor the Modules DAO so that it uses the mongoose model to insert a new module into the database as shown below.
Kanbas/Modules/dao.js
import Database from "../Database/index.js";import model from "./model.js";
export function createModule(module) {
delete module._id
return model.create(module);
// const newModule = { ...module, _id: Date.now().toString() };
// Database.modules = [...Database.modules, newModule];
// return newModule;
}
In the Courses routes, refactor the routing function by adding async and await keywords before the routing and DAO
function calls as shown below.
Kanbas/Courses/routes.js
import * as modulesDao from "../Modules/dao.js";
import * as dao from "./dao.js";
export default function CourseRoutes(app) {
app.post("/api/courses/:courseId/modules", async (req, res) => {
const { courseId } = req.params;
const module = {
...req.body,
course: courseId,
};
const newModule = await modulesDao.createModule(module);
res.send(newModule);
});
...
}
In the Courses client fifile, refactor the client function to use axios with credentials as shown below. Confifirm that new
modules are created in the database and that the course fifield is set to the primary key of the parent course.
src/Kanbas/Courses/client.js
import axios from "axios";
const axiosWithCredentials = axios.create({ withCredentials: true });
const REMOTE_SERVER = process.env.REACT_APP_REMOTE_SERVER;
const COURSES_API = `${REMOTE_SERVER}/api/courses`;
export const createModuleForCourse = async (courseId: string, module: any) => {
const response = await axiosWithCredentials.post(
`${COURSES_API}/${courseId}/modules`,
module
);
return response.data;
};
5.2.3 Retrieving Modules for a Course
Refactor the Modules DAO so that it uses the mongoose model to retrieve modules for a course from the database as
shown below.
Kanbas/Modules/dao.js
import model from "./model.js";
export function findModulesForCourse(courseId) {
return model.find({ course: courseId });
// const { modules } = Database;
// return modules.filter((module) => module.course === courseId);
}
In the Courses routes, refactor the routing function by adding async and await keywords before the routing and DAO
function calls as shown below.
Kanbas/Courses/routes.jsimport * as modulesDao from "../Modules/dao.js";
import * as dao from "./dao.js";
export default function CourseRoutes(app) {
app.get("/api/courses/:courseId/modules", async (req, res) => {
const { courseId } = req.params;
const modules = await modulesDao.findModulesForCourse(courseId);
res.json(modules);
});
...
}
In the Courses client fifile, refactor the client function to use axios with credentials as shown below. Confifirm that modules
are retrieved and displayed from the database for the correct course.
src/Kanbas/Courses/client.js
import axios from "axios";
const axiosWithCredentials = axios.create({ withCredentials: true });
const REMOTE_SERVER = process.env.REACT_APP_REMOTE_SERVER;
const COURSES_API = `${REMOTE_SERVER}/api/courses`;
export const findModulesForCourse = async (courseId: string) => {
const response = await axiosWithCredentials.get(`${COURSES_API}/${courseId}/modules`);
return response.data;
};
5.2.4 Deleting Modules
Refactor the Modules DAO so that it uses the mongoose model to delete modules from the database as shown below.
Kanbas/Modules/dao.js
import model from "./model.js";
export function deleteModule(moduleId) {
return model.deleteOne({ _id: moduleId });
// const { modules } = Database;
// Database.modules = modules.filter((module) => module._id !== moduleId);
}
In the Modules routes, refactor the routing function by adding async and await keywords before the routing and DAO
function calls as shown below.
Kanbas/Modules/routes.js
import * as modulesDao from "./dao.js";
export default function ModuleRoutes(app) {
app.delete("/api/modules/:moduleId", async (req, res) => {
const { moduleId } = req.params;
const status = await modulesDao.deleteModule(moduleId);
res.send(status);
});
}
In the Modules client fifile, refactor the client function to use axios with credentials as shown below. Confifirm that modules
are removed from the database.
src/Kanbas/Courses/Modules/client.js
import axios from "axios";
const axiosWithCredentials = axios.create({ withCredentials: true });
const REMOTE_SERVER = process.env.REACT_APP_REMOTE_SERVER;
const MODULES_API = `${REMOTE_SERVER}/api/modules`;
export const deleteModule = async (moduleId) => {
const response = await axiosWithCredentials.delete(
`${MODULES_API}/${moduleId}`);
return response.data;
};
5.2.5 Updating Modules
Refactor the Modules DAO so that it uses the mongoose model to update modules in the database as shown below.
Kanbas/Modules/dao.js
import model from "./model.js";
export function updateModule(moduleId, moduleUpdates) {
return model.updateOne({ _id: moduleId }, moduleUpdates);
// const { modules } = Database;
// const module = modules.find((module) => module._id === moduleId);
// Object.assign(module, moduleUpdates);
// return module;
}
In the Modules routes, refactor the routing function by adding async and await keywords before the routing and DAO
function calls as shown below.
Kanbas/Modules/routes.js
import * as modulesDao from "./dao.js";
export default function ModuleRoutes(app) {
...
app.put("/api/modules/:moduleId", async (req, res) => {
const { moduleId } = req.params;
const moduleUpdates = req.body;
const status = await modulesDao.updateModule(moduleId, moduleUpdates);
res.send(status);
});
...
In the Modules client fifile, refactor the client function to use axios with credentials as shown below. Confifirm that modules
are updated in the database.
src/Kanbas/Courses/Modules/client.js
import axios from "axios";
const axiosWithCredentials = axios.create({ withCredentials: true });
const REMOTE_SERVER = process.env.REACT_APP_REMOTE_SERVER;
const MODULES_API = `${REMOTE_SERVER}/api/modules`;
...
export const updateModule = async (module: any) => {
const { data } = await axiosWithCredentials.put(
`${MODULES_API}/${module._id}`,
module
);
return data;
};
5.3 Persisting Enrollments in a Database as Many to Many Relations
In Kanbas, a user can be enrolled in several courses, and a course can have many enrollments. An enrollment establishes
a relationship between a user and a course. Since there can be many enrollments where many users can be enrolled (or
associated) in many courses, the enrollments relation is referred to as a many to many relation.5.3.1 Declaring Enrollments as a Many to Many Relationship
The Courses model below, implemented earlier, declares CourseModel as the name of the model for course documents
stored in the courses collection. This name can be used to establish relationships between models and collections.
Kanbas/Courses/model.js
import mongoose from "mongoose";
import schema from "./schema.js";
const model = mongoose.model("CourseModel", schema);
export default model;
Similarly the Users model below declares the name of the model as UserModel for user documents stored in the users
collection.
Kanbas/Users/model.js
import mongoose from "mongoose";
import schema from "./schema.js";
const model = mongoose.model("UserModel", schema);
export default model;
// load mongoose library
// load users schema
// create mongoose model from the schema
// export so it can be used elsewhere
In a new schema fifile shown below, implement a many to many Enrollments relationship that relates user and course
documents stored in the users and courses collections, referred to by their model names CourseModel and UserModel
respectively.
Kanbas/Enrollments/schema.js
import mongoose from "mongoose";
const enrollmentSchema = new mongoose.Schema(
{
course: { type: mongoose.Schema.Types.ObjectId, ref: "CourseModel" },
user: { type: mongoose.Schema.Types.ObjectId, ref: "UserModel" },
grade: Number,
letterGrade: String,
enrollmentDate: Date,
status: {
type: String,
enum: ["ENROLLED", "DROPPED", "COMPLETED"],
default: "ENROLLED",
},
},
{ collection: "enrollments" }
);
export default enrollmentSchema;
Create an Enrollments model fifile as shown below, to CRUD enrollment documents in an enrollments collection.
Kanbas/Enrollments/model.js
import mongoose from "mongoose";
import schema from "./schema.js";
const model = mongoose.model("EnrollmentModel", schema);
export default model;
Create an Enrollments DAO fifile that implements operations that create enrollments, deletes enrollments, and fifilters
enrollments by either a course or user. The following sections describe each of the operations in detail.
Kanbas/Enrollments/dao.js
import model from "./model.js";
export async function findCoursesForUser(userId) {const enrollments = await model.find({ user: userId }).populate("course");
return enrollments.map((enrollment) => enrollment.course);
}
export async function findUsersForCourse(courseId) {
const enrollments = await model.find({ course: courseId }).populate("user");
return enrollments.map((enrollment) => enrollment.user);
}
export function enrollUserInCourse(user, course) {
return model.create({ user, course });
}
export function unenrollUserFromCourse(user, course) {
return model.deleteOne({ user, course });
}
5.3.2 Retrieving Courses for Enrolled Users
Enrollments establish a many to many relationship between users and courses. A common operation consists of fifinding
which documents in one collection are related to documents in the other collection. For instance, given a particular user
we would like to determine which courses are related to that user, e.g., which courses is a user enrolled in. The
fifindCoursesForUser() function below retrieves the enrollment documents for a given user. The enrollments documents
would contain the primary keys for the user and course documents being referenced. The populate() function tells
mongoose to use the value of the primary keys to fetch the actual document referenced by the key. The populate("course")
function replaces the course primary key value in the enrollments document with the actual course document from the
courses collection corresponding to the key's value. The enrollments.map() operation unwraps the enrollments array and
returns a new array with just the course objects.
Kanbas/Enrollments/dao.js
import model from "./model.js";
export async function findCoursesForUser(userId) {
const enrollments = await model.find({ user: userId }).populate("course");
return enrollments.map((enrollment) => enrollment.course);
}
...
In the Users routes, implement a route function fifindCoursesForUser to retrieves courses for a given user. First make sure
there's a logged in user. If the logged in user's role is ADMIN, then respond with all the courses. Use the user's ID to
retrieve the user's courses using the fifindCoursesForUser in the DAO implemented above.
Kanbas/Users/routes.js
import * as dao from "./dao.js";
import * as courseDao from "../Courses/dao.js";
import * as enrollmentsDao from "../Enrollments/dao.js";
export default function UserRoutes(app) {
const findCoursesForUser = async (req, res) => {
const currentUser = req.session["currentUser"];
if (!currentUser) {
res.sendStatus(401);
return;
}
if (currentUser.role === "ADMIN") {
const courses = await courseDao.findAllCourses();
res.json(courses);
return;
}
let { uid } = req.params;
if (uid === "current") {
uid = currentUser._id;
}
const courses = await enrollmentsDao.findCoursesForUser(uid);
res.json(courses);
};
app.get("/api/users/:uid/courses", findCoursesForUser);
...}
In the React.js project, create fifindCoursesForUser() client function to request the courses for a given user ID.
src/Kanbas/Account/client.ts
import axios from "axios";
const axiosWithCredentials = axios.create({ withCredentials: true });
export const REMOTE_SERVER = process.env.REACT_APP_REMOTE_SERVER;
export const USERS_API = `${REMOTE_SERVER}/api/users`;
export const findCoursesForUser = async (userId: string) => {
const response = await axiosWithCredentials.get(`${USERS_API}/${userId}/courses`);
return response.data;
};
In the Kanbas root component, implement an enrolling boolean state variable that toggles true/false to either display all
courses or only the courses the current user is enrolled in. Implement event handler functions fifindCoursesForUser and
fetchCourses as shown below to only fetch the courses a user is enrolled in or, also fetch all the courses. If enrolling, use
both sets of courses to toggle an enrolled flflag in the courses that can be used to display an Enroll/Unenroll button for
each course. Pass enrolling and setEnrolling to the Dashboard so that it knows how to render the courses.
src/Kanbas/index.tsx
import { useSelector } from "react-redux";
import { useEffect, useState } from "react";
import * as courseClient from "./Courses/client";
import * as userClient from "./Account/client";
export default function Kanbas() {
const { currentUser } = useSelector((state: any) => state.accountReducer);
const [courses, setCourses] = useState<any[]>([]);
const [enrolling, setEnrolling] = useState<boolean>(false);
const findCoursesForUser = async () => {
try {
const courses = await userClient.findCoursesForUser(currentUser._id);
setCourses(courses);
} catch (error) {
console.error(error);
}
};
const fetchCourses = async () => {
try {
const allCourses = await courseClient.fetchAllCourses();
const enrolledCourses = await userClient.findCoursesForUser(
currentUser._id
);
const courses = allCourses.map((course: any) => {
if (enrolledCourses.find((c: any) => c._id === course._id)) {
return { ...course, enrolled: true };
} else {
return course;
}
});
setCourses(courses);
} catch (error) {
console.error(error);
}
};
...
useEffect(() => {
if (enrolling) {
fetchCourses();
} else {
findCoursesForUser();
}
}, [currentUser, enrolling]);
...
return (
...
<Dashboard courses={courses} course={course} setCourse={setCourse}
addNewCourse={addNewCourse} deleteCourse={deleteCourse} updateCourse={updateCourse}enrolling={enrolling} setEnrolling={setEnrolling}/>
...
);}
In the Dashboard screen, use the enrolling and setEnrolling to implement a new button that toggles the enrolling boolean
state variable. If enrolling is true, show a Enroll/Unenroll button for each course. If the enrolled flflag is true, show
Unenrolled and Enroll otherwise.
src/Kanbas/Dashboard.ts
...
export default function Dashboard({ ..., enrolling, setEnrolling }
: { ...; enrolling: boolean; setEnrolling: (enrolling: boolean) => void;}) {
const { currentUser } = useSelector((state: any) => state.accountReducer);
...
return (
<div id="wd-dashboard">
<h1 id="wd-dashboard-title">
Dashboard
<button onClick={() => setEnrolling(!enrolling)} className="float-end btn btn-primary" >
{enrolling ? "My Courses" : "All Courses"}
</button>
</h1>
...
{courses.map((course) => (
...
<div className="card-body">
<h5 className="wd-dashboard-course-title card-title">
{enrolling && (
<button className={`btn ${ course.enrolled ? "btn-danger" : "btn-success" } float-end`} >
{course.enrolled ? "Unenroll" : "Enroll"}
</button>
)}
{course.name}
</h5>
...
</div>
))}
...
</div>
...
);}
5.3.3 Enrolling / Unenrolling
In the Enrollments DAO, implement enrollUserInCourse and unenrollUserFromCourse functions as shown below. The
enrollUserInCourse function inserts a new enrollment document in the enrollments collection creating a relation between
a user and the course they are enrolled in. The unenrollUserFromCourse function deletes an existing enrollment document
from the enrollments collection, removing the relation between a user and the course they were enrolled in.
Kanbas/Enrollments/dao.js
import model from "./model.js";
export async function findCoursesForUser(userId) {
const enrollments = await model.find({ user: userId }).populate("course");
return enrollments.map((enrollment) => enrollment.course);
}
export function enrollUserInCourse(user, course) {
return model.create({ user, course });
}
export function unenrollUserFromCourse(user, course) {
return model.deleteOne({ user, course });
}
In the Users routes implement post and delete routes that create or removes an enrollment using the corresponding DAO
functions.Kanbas/Users/routes.js
import * as enrollmentsDao from "../Enrollments/dao.js";
export default function UserRoutes(app) {
const findCoursesForUser = async (req, res) => { ... };
const enrollUserInCourse = async (req, res) => {
let { uid, cid } = req.params;
if (uid === "current") {
const currentUser = req.session["currentUser"];
uid = currentUser._id;
}
const status = await enrollmentsDao.enrollUserInCourse(uid, cid);
res.send(status);
};
const unenrollUserFromCourse = async (req, res) => {
let { uid, cid } = req.params;
if (uid === "current") {
const currentUser = req.session["currentUser"];
uid = currentUser._id;
}
const status = await enrollmentsDao.unenrollUserFromCourse(uid, cid);
res.send(status);
};
app.post("/api/users/:uid/courses/:cid", enrollUserInCourse);
app.delete("/api/users/:uid/courses/:cid", unenrollUserFromCourse);
app.get("/api/users/:uid/courses", findCoursesForUser);
...
}
In the React.js application, create client functions enrollIntoCourse and unenrollFromCourse that request the server to
enroll or unenroll a user from a course. Encode the primary keys of the user and course as part of the path.
src/Kanbas/Account/client.ts
import axios from "axios";
const axiosWithCredentials = axios.create({ withCredentials: true });
export const REMOTE_SERVER = process.env.REACT_APP_REMOTE_SERVER;
export const USERS_API = `${REMOTE_SERVER}/api/users`;
export const findCoursesForUser = async (userId: string) => { ... }
export const enrollIntoCourse = async (userId: string, courseId: string) => {
const response = await axiosWithCredentials.post(`${USERS_API}/${userId}/courses/${courseId}`);
return response.data;
};
export const unenrollFromCourse = async (userId: string, courseId: string) => {
const response = await axiosWithCredentials.delete(`${USERS_API}/${userId}/courses/${courseId}`);
return response.data;
};
In the Kanbas root component, implement updateEnrollment() event handler which calls the client functions
enrollIntoCourse or unenrollFromCourse based on whether enrolled is true of false, as shown below. Pass the
updateEnrollment() function to the Dashboard screen.
src/Kanbas/index.ts
import * as userClient from "./Account/client";
export default function Kanbas() {
const { currentUser } = useSelector((state: any) => state.accountReducer);
const [enrolling, setEnrolling] = useState<boolean>(false);
const findCoursesForUser = async () => { ... };
const updateEnrollment = async (courseId: string, enrolled: boolean) => {
if (enrolled) {
await userClient.enrollIntoCourse(currentUser._id, courseId);
} else {
await userClient.unenrollFromCourse(currentUser._id, courseId);
}
setCourses(
courses.map((course) => {
if (course._id === courseId) {
return { ...course, enrolled: enrolled };
} else {return course;
}
})
);
};
const fetchCourses = async () => { ... }
...
useEffect(() => { ... }
...
return (
...
<Dashboard courses={courses} course={course} setCourse={setCourse}
addNewCourse={addNewCourse} deleteCourse={deleteCourse} updateCourse={updateCourse}
enrolling={enrolling} setEnrolling={setEnrolling} updateEnrollment={updateEnrollment}/>
...
);}
In the Dashboard screen implement an onClick event handler that calls updateEnrollment() with the course's ID and
toggles the course's enrolled flflag, causing the user to enroll or unenroll accordingly. Confifirm that users can enroll and
unenroll from any course.
src/Kanbas/Dashboard.ts
...
export default function Dashboard({ ..., enrolling, setEnrolling, updateEnrollment }
: { ...; enrolling: boolean; setEnrolling: (enrolling: boolean) => void;
updateEnrollment: (courseId: string, enrolled: boolean) => void }) {
const { currentUser } = useSelector((state: any) => state.accountReducer);
...
return (
<div id="wd-dashboard">
...
{courses.map((course) => (
...
<div className="card-body">
<h5 className="wd-dashboard-course-title card-title">
{enrolling && (
<button onClick={(event) => {
event.preventDefault();
updateEnrollment(course._id, !course.enrolled);
}}
className={`btn ${ course.enrolled ? "btn-danger" : "btn-success" } float-end`} >
{course.enrolled ? "Unenroll" : "Enroll"}
</button>
)}
{course.name}
</h5>
...
</div>
))}
...
</div>
...
);}
5.3.4 Enrolling the Author
When a user creates a course, they should be automatically be enrolled in the course, otherwise they would not see the
course in their Dashboard when they signin. In the Courses routes, in the route that creates new courses, use the IDs of the
signed in user and the new course to enroll the user in the new course. Use the code below as a guide. Confifirm that users
that create courses are enrolled in the new course.
Kanbas/Courses/routes.js
import * as enrollmentsDao from "../Enrollments/dao.js";
export default function CourseRoutes(app) {
app.post("/api/courses", async (req, res) => {const course = await dao.createCourse(req.body);
const currentUser = req.session["currentUser"];
if (currentUser) {
await enrollmentsDao.enrollUserInCourse(currentUser._id, course._id);
}
res.json(course);
});
5.3.5 Retrieving a Course's Students (On Your Own)
The Users screen implement earlier uses the PeopleTable to display all the users in the database. The People route
implemented earlier under the Courses screen, should display the users in the current course. Reimplement the People
route so that the PeopleTable only displays the users that are enrolled in a particular course when navigating to the People
link. In the Courses routes use the enrollments DAO fifindUsersForCourse function to retrieve the users enrolled in a course.
Kanbas/Courses/routes.ts
import * as enrollmentsDao from "../Enrollments/dao.js";
export default function CourseRoutes(app) {
const findUsersForCourse = async (req, res) => {
const { cid } = req.params;
const users = await enrollmentsDao.findUsersForCourse(cid);
res.json(users);
};
app.get("/api/courses/:cid/users", findUsersForCourse);
...
}
In Courses client implement fifindUsersForCourse() client function to retrieve the users for a given course. Use the Courses
routes and client to display the users enrolled in a course when navigating to a course's People route.
src/Kanbas/Courses/client.ts
import axios from "axios";
const axiosWithCredentials = axios.create({ withCredentials: true });
const REMOTE_SERVER = process.env.REACT_APP_REMOTE_SERVER;
const COURSES_API = `${REMOTE_SERVER}/api/courses`;
export const findUsersForCourse = async (courseId: string) => {
const response = await axios.get(`${COURSES_API}/${courseId}/users`);
return response.data;
};
5.4 Assignments (On Your Own)
Implement schema, model, DAO, routes, and client fifiles so that the Assignments and AssignmentsEditor screens display
assignments stored in a database. Users should be able to display assignments in a course, create new assignments,
update assignments, and delete existing assignments.
6 Deliverables
As a deliverable, make sure you complete all the lab exercises, mongoose schemas, models, DAOs, React components,
and they behave as described. For both the React and Node repositories, all your work should be done in a branch called
a6. When done, add, commit and push both branches to their respective GitHub repositories. Deploy the new branches to
Netlify and Render (or Heroku) and confifirm they integrate. If you are using Render, it does not create a separate branch
deployment like Netlify so you'll have to deploy to an entirely different Web service so that you don't trample the previousassignment while the TAs are still grading. The Node server application running on Render will need to be confifigured to
interact with the a6 Netlify branch deployment. All the exercises should work remotely just as well as locally. The Kanbas
Dashboard should display the courses and modules from the database. As a deliverable in Canvas, submit the URL to the
a6 branch deployment of your React.js application running on Netlify.
版权所有:留学生编程辅导网 2020 All Rights Reserved 联系方式:QQ:821613408 微信:horysk8 电子信箱:[email protected]
免责声明:本站部分内容从网络整理而来,只供参考!如有版权问题可联系本站删除。