Initial commit 🚀

This commit is contained in:
Jonas Schmedtmann
2019-06-13 15:43:15 +01:00
commit 7f81af0ddf
1052 changed files with 2123177 additions and 0 deletions

View 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;

View 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;

View 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;

View 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;