Skip to content

Commit

Permalink
mongoose advanced querying & relationships
Browse files Browse the repository at this point in the history
  • Loading branch information
marieaurore123 committed Feb 6, 2021
1 parent bf18c50 commit 878a992
Show file tree
Hide file tree
Showing 11 changed files with 346 additions and 15 deletions.
12 changes: 4 additions & 8 deletions _data/bootcamps.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
"housing": true,
"jobAssistance": true,
"jobGuarantee": false,
"acceptGi": true,
"averageCost": 10000
"acceptGi": true
},
{
"_id": "5d713a66ec8f2b88b8f830b8",
Expand All @@ -28,8 +27,7 @@
"housing": false,
"jobAssistance": true,
"jobGuarantee": false,
"acceptGi": true,
"averageCost": 12000
"acceptGi": true
},
{
"_id": "5d725a037b292f5f8ceff787",
Expand All @@ -44,8 +42,7 @@
"housing": false,
"jobAssistance": false,
"jobGuarantee": false,
"acceptGi": false,
"averageCost": 8000
"acceptGi": false
},
{
"_id": "5d725a1b7b292f5f8ceff788",
Expand All @@ -65,7 +62,6 @@
"housing": false,
"jobAssistance": true,
"jobGuarantee": true,
"acceptGi": true,
"averageCost": 5000
"acceptGi": true
}
]
58 changes: 54 additions & 4 deletions controllers/bootcamps.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
const geocider = require('../utils/geocoder');
const Bootcamp = require('../models/Bootcamp');
const ErrorResponse = require('../utils/errorResponse');
const asyncHandler = require('../middleware/async');
const geocoder = require('../utils/geocoder');
const { remove } = require('../models/Bootcamp');
const path = require('path'); //core node module

/**
* NOTES
Expand Down Expand Up @@ -44,7 +43,7 @@ exports.getBootcamps = asyncHandler(async (req, res, next) => {
queryStr = queryStr.replace(/\b(gt|gte|lt|lte|in)\b/g, match => `$${match}`);

// Finding resource
query = Bootcamp.find(JSON.parse(queryStr)); //queryString is a string, needs to be a JS object
query = Bootcamp.find(JSON.parse(queryStr)).populate('courses'); //queryString is a string, needs to be a JS object

// Select Fields ie. ?select=name,description will return only name and description of all entries
if (req.query.select) {
Expand Down Expand Up @@ -157,10 +156,16 @@ exports.updateBootcamp = asyncHandler(async (req, res, next) => {
// @route DELETE /api/v1/bootcamps/:id
// @access Private
exports.deleteBootcamp = asyncHandler(async (req, res, next) => {
const bootcamp = await Bootcamp.findByIdAndDelete(req.params.id);
// const bootcamp = await Bootcamp.findByIdAndDelete(req.params.id); --> "findbyIdAndDelete" will not trigger the delete middleware in Bootcamp.js model

// INSTEAD, 1. get the bootcamp
const bootcamp = await Bootcamp.findById(req.params.id);

if (!bootcamp) return next(new ErrorResponse(`Bootcamp not found with id of ${req.params.id}`, 404));

// 2. this will trigger middleware
bootcamp.remove();

res.status(200).json({success: true, data: {}});
});

Expand Down Expand Up @@ -189,4 +194,49 @@ exports.getBootcampsInRadius = asyncHandler(async (req, res, next) => {
count: bootcamps.length,
data: bootcamps
});
});

// @desc Upload a photo for bootcamp
// @route PUT /api/v1/bootcamps/:id/photo
// @access Private
exports.bootcampPhotoUpload = asyncHandler(async (req, res, next) => {

const bootcamp = await Bootcamp.findById(req.params.id);

if (!bootcamp) return next(new ErrorResponse(`Bootcamp not found with id of ${req.params.id}`, 404));

if (!req.files) return next(new ErrorResponse(`Please uoload file`, 400));

// console.log(req.files); --> see how file is sent by postman

const file = req.files.file;

// Make sure the image is a photo
if (!file.mimetype.startsWith('image')) {
return next(new ErrorResponse(`Please upload an image file`, 400));
}

// Check file size
if (file.size > process.env.MAX_FILE_UPLOAD) {
return next(new ErrorResponse(`Please upload an image less than ${process.env.MAX_FILE_UPLOAD}`, 400));
}

// Need to change file name (in case someone adds another photo with same name, it won't get overwritten)
file.name = `photo_${bootcamp._id}${path.parse(file.name).ext}`; // get extension of file name before changing it

//console.log(file.name);

file.mv(`${process.env.FILE_UPLOAD_PATH}/${file.name}`, async err => {
if (err){
console.error(err);
return next(new ErrorResponse(`Problem with file upload`, 500));
}

await Bootcamp.findByIdAndUpdate(req.params.id, { photo: file.name });

res.status(200).json({
success: true,
data: file.name
})
});
});
115 changes: 115 additions & 0 deletions controllers/courses.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
const Course = require('../models/Course');
const Bootcamp = require('../models/Bootcamp');
const ErrorResponse = require('../utils/errorResponse');
const asyncHandler = require('../middleware/async');

// @desc Get all courses
// @route GET /api/v1/courses
// @route GET /api/v1/bootcamps/:bootcampid/courses
// @access Public

exports.getCourses = asyncHandler(async (req, res, next) => {
let query;

if (req.params.bootcampId) {
query = Course.find({ bootcamp: req.params.bootcampId});
}
else {
query = Course.find().populate({ // populate the course's bootcamp field with the bootcamp name and description
path: 'bootcamp',
select: 'name description'
});
}

const courses = await query;

res.status(200).json({
success: true,
count: courses.length,
data: courses
});
});

// @desc Get single course by id
// @route GET /api/v1/courses/:id
// @access Public

exports.getCourse = asyncHandler(async (req, res, next) => {
const course = await Course.findById(req.params.id).populate({
path: 'bootcamp',
select: 'name description'
});

if (!course) {
return next(new ErrorResponse(`No course with id of ${req.params.id}`), 404);
}

res.status(200).json({
success: true,
data: course
});
});

// @desc Add course
// @route POST /api/v1/bootcamps/:bootcampid/courses
// @access Private

exports.addCourse = asyncHandler(async (req, res, next) => {
req.body.bootcamp = req.params.bootcampId; //req.body.bootcamp refers to the 'bootcamp' field in the Course model

const bootcamp = await Bootcamp.findById(req.params.bootcampId);

if (!bootcamp) {
return next(new ErrorResponse(`No bootcamp with id of ${req.params.bootcampId}`), 404);
}

const course = await Course.create(req.body);

res.status(200).json({
success: true,
data: course
});
});

// @desc Update a course
// @route PUT /api/v1/courses/:id
// @access Private

exports.updateCourse = asyncHandler(async (req, res, next) => {

let course = await Course.findById(req.params.id);

if (!course) {
return next(new ErrorResponse(`No course with id of ${req.params.id}`), 404);
}

course = await Course.findByIdAndUpdate(req.params.id, req.body, {
new: true,
runValidators: true
});

res.status(200).json({
success: true,
data: course
});
});

// @desc Delete a course
// @route DELETE /api/v1/courses/:id
// @access Private

exports.deleteCourse = asyncHandler(async (req, res, next) => {

const course = await Course.findById(req.params.id);

if (!course) {
return next(new ErrorResponse(`No course with id of ${req.params.id}`), 404);
}

await course.remove(); // cannot use findByIdAndDelete due to course middleware

res.status(200).json({
success: true,
data: {}
});
});
23 changes: 23 additions & 0 deletions models/Bootcamp.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ const BootcampSchema = new mongoose.Schema({
type: Date,
default: Date.now
}
}, {
toJSON: { virtuals: true},
toObject: { virtuals: true }
});

/**
Expand Down Expand Up @@ -139,4 +142,24 @@ BootcampSchema.pre('save', async function(next) {
next();
});

// Cascade delete courses when bootcamp is deleted (if bootcamp is deleted, delete all cooresponding courses)
BootcampSchema.pre('remove', async function (next) {
console.log(`Courses are being removed from bootcamp ${this._id}`);

// Delete courses with bootcamp field = this._id (current bootcamp)
// Note: this must be "pre" middleware (if it were "post", since we are deleting the bootcamp, we would not be able to access "this" fields)
await this.model('Course').deleteMany({ bootcamp: this._id });
next();
})

// Reverse Populate with Virtuals (populate Bootcamp object with all courses it coresponds to)
// This creates a new field called 'courses' in Bootcamp model that holds all courses coresponding to bootcamp
// NOTE: this does NOT get saved on the DB
BootcampSchema.virtual('courses', {
ref: 'Course',
localField: '_id',
foreignField: 'bootcamp', // field in the Course model that is tageted
justOne: false // need an array
})

module.exports = mongoose.model('Bootcamp', BootcampSchema);
42 changes: 42 additions & 0 deletions models/Course.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const mongoose = require('mongoose');
const Bootcamp = require('./Bootcamp');

const CourseSchema = new mongoose.Schema({
title: {
Expand Down Expand Up @@ -38,4 +39,45 @@ const CourseSchema = new mongoose.Schema({
}
});

/**
* MONGOOSE MIDDLEWARE
*/

// Static method to get avg of course tuitions (static method gets called on Model object)
// NOTE: each bootcamp has an array of courses --> this function calculates the average cost of a particular bootcamp's courses

CourseSchema.statics.getAverageCost = async function(bootcampId) {
//console.log('Calculating avg cost... '.blue);

const obj = await this.aggregate([ // aggregate funciton called on entire Course model
{
$match: { bootcamp: bootcampId }
},
{
$group: {
_id: '$bootcamp',
averageCost: { $avg: '$tuition' }
}
}
]); // ie. console.log(obj) --> [ { _id: 5d725a1b7b292f5f8ceff788, averageCost: 11250 } ], where _id is id of a bootcamp

// Save averageCost to DB (at Bootcamp object)
try {
// get Bootcamp field from Course
await this.model('Bootcamp').findByIdAndUpdate(bootcampId, { averageCost: Math.ceil(obj[0].averageCost / 10) * 10 }); // log obj to understand
} catch (err) {
console.error(err);
}
}

//Call getAverage Cost after save
CourseSchema.post('save', function() {
this.constructor.getAverageCost(this.bootcamp);
});

// Call getAverageCost before remove
CourseSchema.pre('remove', function() {
this.constructor.getAverageCost(this.bootcamp);
});

module.exports = mongoose.model('Course', CourseSchema);
Loading

0 comments on commit 878a992

Please sign in to comment.