From bf18c50a1d8c95420aac325c61f07610fb0e46e5 Mon Sep 17 00:00:00 2001 From: Garance Buricatu Date: Thu, 4 Feb 2021 21:59:32 -0500 Subject: [PATCH] mongoose queries and course model --- _data/bootcamps.json | 12 +++-- controllers/bootcamps.js | 109 ++++++++++++++++++++++++++++++++++++++- models/Bootcamp.js | 4 ++ models/Course.js | 41 +++++++++++++++ routes/bootcamps.js | 3 +- seeder.js | 57 ++++++++++++++++++++ 6 files changed, 219 insertions(+), 7 deletions(-) create mode 100644 models/Course.js create mode 100644 seeder.js diff --git a/_data/bootcamps.json b/_data/bootcamps.json index c117ee8..da806f1 100644 --- a/_data/bootcamps.json +++ b/_data/bootcamps.json @@ -12,7 +12,8 @@ "housing": true, "jobAssistance": true, "jobGuarantee": false, - "acceptGi": true + "acceptGi": true, + "averageCost": 10000 }, { "_id": "5d713a66ec8f2b88b8f830b8", @@ -27,7 +28,8 @@ "housing": false, "jobAssistance": true, "jobGuarantee": false, - "acceptGi": true + "acceptGi": true, + "averageCost": 12000 }, { "_id": "5d725a037b292f5f8ceff787", @@ -42,7 +44,8 @@ "housing": false, "jobAssistance": false, "jobGuarantee": false, - "acceptGi": false + "acceptGi": false, + "averageCost": 8000 }, { "_id": "5d725a1b7b292f5f8ceff788", @@ -62,6 +65,7 @@ "housing": false, "jobAssistance": true, "jobGuarantee": true, - "acceptGi": true + "acceptGi": true, + "averageCost": 5000 } ] diff --git a/controllers/bootcamps.js b/controllers/bootcamps.js index cc8dafd..acf7a25 100644 --- a/controllers/bootcamps.js +++ b/controllers/bootcamps.js @@ -1,6 +1,9 @@ +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'); /** * NOTES @@ -13,10 +16,85 @@ const asyncHandler = require('../middleware/async'); // @route GET /api/v1/bootcamps // @access Public exports.getBootcamps = asyncHandler(async (req, res, next) => { - const bootcamps = await Bootcamp.find(); + + //console.log(req.query); --> obtain all queries from url as a javascript object + + /** + * REQ.QUERY EXAMPLES: + * ?location.state=MA&housing=true --> logs as --> { 'location.state': 'MA', housing: 'true' } + * ?averageCost[lte]=1000 --> logs as --> { averageCost: { lte: '1000' } } + */ + + let query; + + // Copy req.query + const reqQuery = { ...req.query }; + + // Fields to exclude + const removeFields = ['select', 'sort', 'page', 'limit']; + + // Loop thru removeFields and delete them from reqQuery + removeFields.forEach(param => delete reqQuery[param]); + + // Create query string + let queryStr = JSON.stringify(reqQuery); // req.query is a JS object --> needs to be a string + + // In Mongoose, need: find( { qty: { $gt: 20 } } ) for gt/gte/lt/lte/in --> concatenate a "$" to these values in the req.query + // Note: ?careers[in]=Business --> checks if "Business" is in "careers" array + 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 + + // Select Fields ie. ?select=name,description will return only name and description of all entries + if (req.query.select) { + + // client will send ?select=name,description but according to Mongoose DB needs to be a string seperated by spaces + // see mongoosejs.com/docs/queries.hyml + const fields = req.query.select.split(',').join(' '); // 'select' is the field + query = query.select(fields); // 'select' is a mongoose function + } + + // Sort ie. $sort=name will sort the entires by alphabetical order of names + if (req.query.sort) { + const sortBy = req.query.sort.split(',').join(' '); + query = query.sort(sortBy); + } else { + query = query.sort('-createdAt'); // (-) makes reverse sort + } + + // Pagination + const page = parseInt(req.query.page, 10) || 1; // page that client is looking at (default: page 1) + const limit = parseInt(req.query.limit, 10) || 25; // entries per page + const startIndex = (page - 1) * limit; // ie. if on page 3 with limit=2, skip the first (2 * 2 = 4) entries (only display the last 2) + const endIndex = page * limit; + const total = await Bootcamp.countDocuments(); // all objects in the database + + query = query.skip(startIndex).limit(limit); + + // Executing Query + const bootcamps = await query; + + // Pagination result --> sent in res, not saved into DB + const pagination = {}; + if (endIndex < total) { // only displays if it is not the last page + pagination.next ={ + page: page + 1, + limit + } + } + + if (startIndex > 0){ // only displays if it is not the first page + pagination.prev ={ + page: page - 1, + limit + } + } + res.status(200).json({ success: true, count: bootcamps.length, + pagination, data: bootcamps }) @@ -77,11 +155,38 @@ exports.updateBootcamp = asyncHandler(async (req, res, next) => { // @desc Delete Single Bootcamp // @route DELETE /api/v1/bootcamps/:id -// @access Provate +// @access Private exports.deleteBootcamp = asyncHandler(async (req, res, next) => { const bootcamp = await Bootcamp.findByIdAndDelete(req.params.id); if (!bootcamp) return next(new ErrorResponse(`Bootcamp not found with id of ${req.params.id}`, 404)); res.status(200).json({success: true, data: {}}); +}); + +// @desc Get bootcamps within a radius +// @route GET /api/v1/bootcamps/radius/:zipcode/:distance +// @access Public +exports.getBootcampsInRadius = asyncHandler(async (req, res, next) => { + const { zipcode, distance } = req.params; + + // get lat/long from geocoder + const loc = await geocoder.geocode(zipcode); + const lat = loc[0].latitude; + const lng = loc[0].longitude; + + // Calculate radius using radians + // Divide distance by radius of earth + // Earth radius = 3,963 mi / 6,378 km + const radius = distance / 3963; + + const bootcamps = await Bootcamp.find({ + location: { $geoWithin: { $centerSphere: [ [lng, lat], radius ]} } + }); + + res.status(200).json({ + success: true, + count: bootcamps.length, + data: bootcamps + }); }); \ No newline at end of file diff --git a/models/Bootcamp.js b/models/Bootcamp.js index b44fc5a..0ae1765 100644 --- a/models/Bootcamp.js +++ b/models/Bootcamp.js @@ -101,6 +101,10 @@ const BootcampSchema = new mongoose.Schema({ } }); +/** + * MONGOOSE MIDDLEWARE + */ + // Create bootcamp slug from the name BootcampSchema.pre('save', function(next) { // this will run before saving diff --git a/models/Course.js b/models/Course.js new file mode 100644 index 0000000..47a7a8d --- /dev/null +++ b/models/Course.js @@ -0,0 +1,41 @@ +const mongoose = require('mongoose'); + +const CourseSchema = new mongoose.Schema({ + title: { + type: String, + trim: true, + required: [true, 'Please add a course title'] + }, + description: { + type: String, + required: [true, 'Please add a description'] + }, + weeks: { + type: String, + required: [true, 'Please add number of weeks'] + }, + tuition: { + type: Number, + required: [true, 'Please add a tuition cost'] + }, + minimumSkill: { + type: String, + required: [true, 'Please add a minimum skill'], + enum: ['beginner', 'intermediate', 'advanced'] + }, + scholarshipAvaliable: { + type: Boolean, + default: false + }, + date: { + type: Date, + default: Date.now + }, + bootcamp: { + type: mongoose.Schema.ObjectId, + ref: 'Bootcamp', // reference the Bootcamp model + required: true + } +}); + +module.exports = mongoose.model('Course', CourseSchema); \ No newline at end of file diff --git a/routes/bootcamps.js b/routes/bootcamps.js index 4ff6e84..93889a2 100644 --- a/routes/bootcamps.js +++ b/routes/bootcamps.js @@ -13,8 +13,9 @@ const router = express.Router(); // allows api routes to be in different file th // However, API logic will be in a middleware file (see ./controllers/bootcamps.js) //1. bring in API names from '../controller/bootcamps - const { getBootcamps, getBootcamp, createBootcamp, updateBootcamp, deleteBootcamp } = require('../controllers/bootcamps') + const { getBootcamps, getBootcamp, createBootcamp, updateBootcamp, deleteBootcamp, getBootcampsInRadius } = require('../controllers/bootcamps') //2. + router.route('/radius/:zipcode/:distance').get(getBootcampsInRadius); router.route('/').get(getBootcamps).post(createBootcamp); router.route('/:id').get(getBootcamp).put(updateBootcamp).delete(deleteBootcamp); diff --git a/seeder.js b/seeder.js new file mode 100644 index 0000000..51f8a78 --- /dev/null +++ b/seeder.js @@ -0,0 +1,57 @@ +const fs = require('fs'); +const mongoose = require('mongoose'); +const colors = require('colors'); +const dotenv = require('dotenv'); + +// Load env variables +dotenv.config({ path: './config/config.env'}); + +// Load models +const Bootcamp = require('./models/Bootcamp'); +const Course = require('./models/Course'); + +// Connect to DB +mongoose.connect(process.env.MONGO_URI, { + useNewUrlParser: true, + useCreateIndex: true, + useFindAndModify: false, + useUnifiedTopology: true +}); + +// Read JSON files +// dirname = curernt directory name +const bootcamps = JSON.parse(fs.readFileSync(`${__dirname}/_data/bootcamps.json`, 'utf-8')); +const courses = JSON.parse(fs.readFileSync(`${__dirname}/_data/courses.json`, 'utf-8')); + + +// Import into DB +const importData = async () => { + try { + await Bootcamp.create(bootcamps); + await Course.create(courses); + console.log('Data Imported...'.green.inverse); + process.exit(); + } catch (err) { + console.error(err); + } +} + +// Delete Data +const deleteData = async () => { + try { + await Bootcamp.deleteMany(); + await Course.deleteMany(); + console.log('Data Destroyed...'.red.inverse); + process.exit(); + } catch (err) { + console.error(err); + } +} + +// When seeder is run, allow argument to know whether to us Import or Delete function +if (process.argv[2] === '-i') { // terminal: node seeder -i + importData(); +} +else if (process.argv[2] === '-d'){ // terminal: node seeder -d + deleteData(); +} \ No newline at end of file