mirror of
https://github.com/kevin-DL/complete-node-bootcamp.git
synced 2026-01-17 05:24:57 +00:00
Initial commit 🚀
This commit is contained in:
38
4-natours/after-section-14/models/bookingModel.js
Normal file
38
4-natours/after-section-14/models/bookingModel.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const bookingSchema = new mongoose.Schema({
|
||||
tour: {
|
||||
type: mongoose.Schema.ObjectId,
|
||||
ref: 'Tour',
|
||||
required: [true, 'Booking must belong to a Tour!']
|
||||
},
|
||||
user: {
|
||||
type: mongoose.Schema.ObjectId,
|
||||
ref: 'User',
|
||||
required: [true, 'Booking must belong to a User!']
|
||||
},
|
||||
price: {
|
||||
type: Number,
|
||||
require: [true, 'Booking must have a price.']
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now()
|
||||
},
|
||||
paid: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
|
||||
bookingSchema.pre(/^find/, function(next) {
|
||||
this.populate('user').populate({
|
||||
path: 'tour',
|
||||
select: 'name'
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
const Booking = mongoose.model('Booking', bookingSchema);
|
||||
|
||||
module.exports = Booking;
|
||||
103
4-natours/after-section-14/models/reviewModel.js
Normal file
103
4-natours/after-section-14/models/reviewModel.js
Normal file
@@ -0,0 +1,103 @@
|
||||
// review / rating / createdAt / ref to tour / ref to user
|
||||
const mongoose = require('mongoose');
|
||||
const Tour = require('./tourModel');
|
||||
|
||||
const reviewSchema = new mongoose.Schema(
|
||||
{
|
||||
review: {
|
||||
type: String,
|
||||
required: [true, 'Review can not be empty!']
|
||||
},
|
||||
rating: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
tour: {
|
||||
type: mongoose.Schema.ObjectId,
|
||||
ref: 'Tour',
|
||||
required: [true, 'Review must belong to a tour.']
|
||||
},
|
||||
user: {
|
||||
type: mongoose.Schema.ObjectId,
|
||||
ref: 'User',
|
||||
required: [true, 'Review must belong to a user']
|
||||
}
|
||||
},
|
||||
{
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
}
|
||||
);
|
||||
|
||||
reviewSchema.index({ tour: 1, user: 1 }, { unique: true });
|
||||
|
||||
reviewSchema.pre(/^find/, function(next) {
|
||||
// this.populate({
|
||||
// path: 'tour',
|
||||
// select: 'name'
|
||||
// }).populate({
|
||||
// path: 'user',
|
||||
// select: 'name photo'
|
||||
// });
|
||||
|
||||
this.populate({
|
||||
path: 'user',
|
||||
select: 'name photo'
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
reviewSchema.statics.calcAverageRatings = async function(tourId) {
|
||||
const stats = await this.aggregate([
|
||||
{
|
||||
$match: { tour: tourId }
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$tour',
|
||||
nRating: { $sum: 1 },
|
||||
avgRating: { $avg: '$rating' }
|
||||
}
|
||||
}
|
||||
]);
|
||||
// console.log(stats);
|
||||
|
||||
if (stats.length > 0) {
|
||||
await Tour.findByIdAndUpdate(tourId, {
|
||||
ratingsQuantity: stats[0].nRating,
|
||||
ratingsAverage: stats[0].avgRating
|
||||
});
|
||||
} else {
|
||||
await Tour.findByIdAndUpdate(tourId, {
|
||||
ratingsQuantity: 0,
|
||||
ratingsAverage: 4.5
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
reviewSchema.post('save', function() {
|
||||
// this points to current review
|
||||
this.constructor.calcAverageRatings(this.tour);
|
||||
});
|
||||
|
||||
// findByIdAndUpdate
|
||||
// findByIdAndDelete
|
||||
reviewSchema.pre(/^findOneAnd/, async function(next) {
|
||||
this.r = await this.findOne();
|
||||
// console.log(this.r);
|
||||
next();
|
||||
});
|
||||
|
||||
reviewSchema.post(/^findOneAnd/, async function() {
|
||||
// await this.findOne(); does NOT work here, query has already executed
|
||||
await this.r.constructor.calcAverageRatings(this.r.tour);
|
||||
});
|
||||
|
||||
const Review = mongoose.model('Review', reviewSchema);
|
||||
|
||||
module.exports = Review;
|
||||
191
4-natours/after-section-14/models/tourModel.js
Normal file
191
4-natours/after-section-14/models/tourModel.js
Normal file
@@ -0,0 +1,191 @@
|
||||
const mongoose = require('mongoose');
|
||||
const slugify = require('slugify');
|
||||
// const User = require('./userModel');
|
||||
// const validator = require('validator');
|
||||
|
||||
const tourSchema = new mongoose.Schema(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: [true, 'A tour must have a name'],
|
||||
unique: true,
|
||||
trim: true,
|
||||
maxlength: [40, 'A tour name must have less or equal then 40 characters'],
|
||||
minlength: [10, 'A tour name must have more or equal then 10 characters']
|
||||
// validate: [validator.isAlpha, 'Tour name must only contain characters']
|
||||
},
|
||||
slug: String,
|
||||
duration: {
|
||||
type: Number,
|
||||
required: [true, 'A tour must have a duration']
|
||||
},
|
||||
maxGroupSize: {
|
||||
type: Number,
|
||||
required: [true, 'A tour must have a group size']
|
||||
},
|
||||
difficulty: {
|
||||
type: String,
|
||||
required: [true, 'A tour must have a difficulty'],
|
||||
enum: {
|
||||
values: ['easy', 'medium', 'difficult'],
|
||||
message: 'Difficulty is either: easy, medium, difficult'
|
||||
}
|
||||
},
|
||||
ratingsAverage: {
|
||||
type: Number,
|
||||
default: 4.5,
|
||||
min: [1, 'Rating must be above 1.0'],
|
||||
max: [5, 'Rating must be below 5.0'],
|
||||
set: val => Math.round(val * 10) / 10 // 4.666666, 46.6666, 47, 4.7
|
||||
},
|
||||
ratingsQuantity: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
price: {
|
||||
type: Number,
|
||||
required: [true, 'A tour must have a price']
|
||||
},
|
||||
priceDiscount: {
|
||||
type: Number,
|
||||
validate: {
|
||||
validator: function(val) {
|
||||
// this only points to current doc on NEW document creation
|
||||
return val < this.price;
|
||||
},
|
||||
message: 'Discount price ({VALUE}) should be below regular price'
|
||||
}
|
||||
},
|
||||
summary: {
|
||||
type: String,
|
||||
trim: true,
|
||||
required: [true, 'A tour must have a description']
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
imageCover: {
|
||||
type: String,
|
||||
required: [true, 'A tour must have a cover image']
|
||||
},
|
||||
images: [String],
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now(),
|
||||
select: false
|
||||
},
|
||||
startDates: [Date],
|
||||
secretTour: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
startLocation: {
|
||||
// GeoJSON
|
||||
type: {
|
||||
type: String,
|
||||
default: 'Point',
|
||||
enum: ['Point']
|
||||
},
|
||||
coordinates: [Number],
|
||||
address: String,
|
||||
description: String
|
||||
},
|
||||
locations: [
|
||||
{
|
||||
type: {
|
||||
type: String,
|
||||
default: 'Point',
|
||||
enum: ['Point']
|
||||
},
|
||||
coordinates: [Number],
|
||||
address: String,
|
||||
description: String,
|
||||
day: Number
|
||||
}
|
||||
],
|
||||
guides: [
|
||||
{
|
||||
type: mongoose.Schema.ObjectId,
|
||||
ref: 'User'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
}
|
||||
);
|
||||
|
||||
// tourSchema.index({ price: 1 });
|
||||
tourSchema.index({ price: 1, ratingsAverage: -1 });
|
||||
tourSchema.index({ slug: 1 });
|
||||
tourSchema.index({ startLocation: '2dsphere' });
|
||||
|
||||
tourSchema.virtual('durationWeeks').get(function() {
|
||||
return this.duration / 7;
|
||||
});
|
||||
|
||||
// Virtual populate
|
||||
tourSchema.virtual('reviews', {
|
||||
ref: 'Review',
|
||||
foreignField: 'tour',
|
||||
localField: '_id'
|
||||
});
|
||||
|
||||
// DOCUMENT MIDDLEWARE: runs before .save() and .create()
|
||||
tourSchema.pre('save', function(next) {
|
||||
this.slug = slugify(this.name, { lower: true });
|
||||
next();
|
||||
});
|
||||
|
||||
// tourSchema.pre('save', async function(next) {
|
||||
// const guidesPromises = this.guides.map(async id => await User.findById(id));
|
||||
// this.guides = await Promise.all(guidesPromises);
|
||||
// next();
|
||||
// });
|
||||
|
||||
// tourSchema.pre('save', function(next) {
|
||||
// console.log('Will save document...');
|
||||
// next();
|
||||
// });
|
||||
|
||||
// tourSchema.post('save', function(doc, next) {
|
||||
// console.log(doc);
|
||||
// next();
|
||||
// });
|
||||
|
||||
// QUERY MIDDLEWARE
|
||||
// tourSchema.pre('find', function(next) {
|
||||
tourSchema.pre(/^find/, function(next) {
|
||||
this.find({ secretTour: { $ne: true } });
|
||||
|
||||
this.start = Date.now();
|
||||
next();
|
||||
});
|
||||
|
||||
tourSchema.pre(/^find/, function(next) {
|
||||
this.populate({
|
||||
path: 'guides',
|
||||
select: '-__v -passwordChangedAt'
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// tourSchema.post(/^find/, function(docs, next) {
|
||||
// console.log(`Query took ${Date.now() - this.start} milliseconds!`);
|
||||
// next();
|
||||
// });
|
||||
|
||||
// AGGREGATION MIDDLEWARE
|
||||
// tourSchema.pre('aggregate', function(next) {
|
||||
// this.pipeline().unshift({ $match: { secretTour: { $ne: true } } });
|
||||
|
||||
// console.log(this.pipeline());
|
||||
// next();
|
||||
// });
|
||||
|
||||
const Tour = mongoose.model('Tour', tourSchema);
|
||||
|
||||
module.exports = Tour;
|
||||
117
4-natours/after-section-14/models/userModel.js
Normal file
117
4-natours/after-section-14/models/userModel.js
Normal file
@@ -0,0 +1,117 @@
|
||||
const crypto = require('crypto');
|
||||
const mongoose = require('mongoose');
|
||||
const validator = require('validator');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
const userSchema = new mongoose.Schema({
|
||||
name: {
|
||||
type: String,
|
||||
required: [true, 'Please tell us your name!']
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: [true, 'Please provide your email'],
|
||||
unique: true,
|
||||
lowercase: true,
|
||||
validate: [validator.isEmail, 'Please provide a valid email']
|
||||
},
|
||||
photo: {
|
||||
type: String,
|
||||
default: 'default.jpg'
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: ['user', 'guide', 'lead-guide', 'admin'],
|
||||
default: 'user'
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: [true, 'Please provide a password'],
|
||||
minlength: 8,
|
||||
select: false
|
||||
},
|
||||
passwordConfirm: {
|
||||
type: String,
|
||||
required: [true, 'Please confirm your password'],
|
||||
validate: {
|
||||
// This only works on CREATE and SAVE!!!
|
||||
validator: function(el) {
|
||||
return el === this.password;
|
||||
},
|
||||
message: 'Passwords are not the same!'
|
||||
}
|
||||
},
|
||||
passwordChangedAt: Date,
|
||||
passwordResetToken: String,
|
||||
passwordResetExpires: Date,
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
select: false
|
||||
}
|
||||
});
|
||||
|
||||
userSchema.pre('save', async function(next) {
|
||||
// Only run this function if password was actually modified
|
||||
if (!this.isModified('password')) return next();
|
||||
|
||||
// Hash the password with cost of 12
|
||||
this.password = await bcrypt.hash(this.password, 12);
|
||||
|
||||
// Delete passwordConfirm field
|
||||
this.passwordConfirm = undefined;
|
||||
next();
|
||||
});
|
||||
|
||||
userSchema.pre('save', function(next) {
|
||||
if (!this.isModified('password') || this.isNew) return next();
|
||||
|
||||
this.passwordChangedAt = Date.now() - 1000;
|
||||
next();
|
||||
});
|
||||
|
||||
userSchema.pre(/^find/, function(next) {
|
||||
// this points to the current query
|
||||
this.find({ active: { $ne: false } });
|
||||
next();
|
||||
});
|
||||
|
||||
userSchema.methods.correctPassword = async function(
|
||||
candidatePassword,
|
||||
userPassword
|
||||
) {
|
||||
return await bcrypt.compare(candidatePassword, userPassword);
|
||||
};
|
||||
|
||||
userSchema.methods.changedPasswordAfter = function(JWTTimestamp) {
|
||||
if (this.passwordChangedAt) {
|
||||
const changedTimestamp = parseInt(
|
||||
this.passwordChangedAt.getTime() / 1000,
|
||||
10
|
||||
);
|
||||
|
||||
return JWTTimestamp < changedTimestamp;
|
||||
}
|
||||
|
||||
// False means NOT changed
|
||||
return false;
|
||||
};
|
||||
|
||||
userSchema.methods.createPasswordResetToken = function() {
|
||||
const resetToken = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
this.passwordResetToken = crypto
|
||||
.createHash('sha256')
|
||||
.update(resetToken)
|
||||
.digest('hex');
|
||||
|
||||
// console.log({ resetToken }, this.passwordResetToken);
|
||||
|
||||
this.passwordResetExpires = Date.now() + 10 * 60 * 1000;
|
||||
|
||||
return resetToken;
|
||||
};
|
||||
|
||||
const User = mongoose.model('User', userSchema);
|
||||
|
||||
module.exports = User;
|
||||
Reference in New Issue
Block a user