This document outlines the MongoDB schemas used in the Slot Booking System, including data models, relationships, and validation rules.
The application uses MongoDB with Mongoose ODM for data modeling. The database consists of three main collections:
The User schema handles authentication and role-based access control.
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
maxlength: 100
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, 'Invalid email format']
},
password: {
type: String,
required: true,
minlength: 6
},
role: {
type: String,
enum: ['super_admin', 'club_admin', 'user'],
default: 'user'
},
club: {
type: String,
required: function() { return this.role === 'club_admin'; },
enum: ['Technical Club', 'Cultural Club', 'Sports Club', 'Literary Club', 'Other']
},
avatar: {
type: String,
default: ''
},
isActive: {
type: Boolean,
default: true
},
lastLogin: {
type: Date
}
}, {
timestamps: true
});
user
: Regular users with read-only accessclub_admin
: Club administrators who can book slotssuper_admin
: System administrators with full accessThe Slot schema defines time slots and venue information.
const slotSchema = new mongoose.Schema({
date: {
type: Date,
required: true,
validate: {
validator: function(date) {
return date >= new Date().setHours(0, 0, 0, 0);
},
message: 'Slot date cannot be in the past'
}
},
startTime: {
type: String,
required: true,
match: [/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, 'Invalid time format (HH:MM)']
},
endTime: {
type: String,
required: true,
match: [/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, 'Invalid time format (HH:MM)'],
validate: {
validator: function(endTime) {
return endTime > this.startTime;
},
message: 'End time must be after start time'
}
},
venue: {
type: String,
required: true,
enum: [
'Auditorium',
'Seminar Hall',
'Ground',
'Classroom Block A',
'Lab Building',
'Conference Room',
'Library Hall'
]
},
capacity: {
type: Number,
required: true,
min: [1, 'Capacity must be at least 1'],
max: [1000, 'Capacity cannot exceed 1000']
},
status: {
type: String,
enum: ['available', 'booked', 'cancelled', 'maintenance'],
default: 'available'
},
bookedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
default: null
},
eventName: {
type: String,
maxlength: 200
},
eventDescription: {
type: String,
maxlength: 1000
},
recurringPattern: {
type: String,
enum: ['none', 'daily', 'weekly', 'monthly'],
default: 'none'
},
recurringEnd: {
type: Date
}
}, {
timestamps: true
});
available
: Open for bookingbooked
: Reserved by a clubcancelled
: Cancelled bookingmaintenance
: Under maintenance/unavailableThe Booking schema manages booking requests and event details.
const bookingSchema = new mongoose.Schema({
slot: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Slot',
required: true
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
club: {
type: String,
required: true,
enum: ['Technical Club', 'Cultural Club', 'Sports Club', 'Literary Club', 'Other']
},
eventName: {
type: String,
required: true,
trim: true,
maxlength: 200
},
eventDescription: {
type: String,
required: true,
maxlength: 1000
},
expectedParticipants: {
type: Number,
required: true,
min: [1, 'Expected participants must be at least 1'],
validate: {
validator: function(participants) {
// Validate against slot capacity when populated
return true; // Custom validation in controller
},
message: 'Expected participants exceed slot capacity'
}
},
status: {
type: String,
enum: ['pending', 'approved', 'rejected', 'cancelled'],
default: 'pending'
},
requirements: {
type: [String],
default: [],
validate: {
validator: function(requirements) {
return requirements.length <= 10;
},
message: 'Cannot have more than 10 requirements'
}
},
contactPerson: {
name: {
type: String,
required: true,
maxlength: 100
},
phone: {
type: String,
required: true,
match: [/^[0-9]{10}$/, 'Phone number must be 10 digits']
},
email: {
type: String,
required: true,
match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, 'Invalid email format']
}
},
approvedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
approvalDate: {
type: Date
},
rejectionReason: {
type: String,
maxlength: 500
},
specialInstructions: {
type: String,
maxlength: 500
}
}, {
timestamps: true
});
pending
: Awaiting approvalapproved
: Approved and confirmedrejected
: Rejected with reasoncancelled
: Cancelled by user or admin// Populate user details in booking
const booking = await Booking.findById(id)
.populate('user', 'name email club')
.populate('slot', 'date startTime endTime venue')
.populate('approvedBy', 'name');
// Populate booking details in slot
const slot = await Slot.findById(id)
.populate('bookedBy', 'name club');
// User collection indexes
userSchema.index({ email: 1 }, { unique: true });
userSchema.index({ role: 1 });
userSchema.index({ club: 1 });
// Slot collection indexes
slotSchema.index({ date: 1, venue: 1 });
slotSchema.index({ status: 1 });
slotSchema.index({ date: 1, startTime: 1, endTime: 1 });
// Booking collection indexes
bookingSchema.index({ user: 1 });
bookingSchema.index({ slot: 1 });
bookingSchema.index({ status: 1 });
bookingSchema.index({ club: 1 });
bookingSchema.index({ createdAt: -1 });
// Hash password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
// Validate slot availability before booking
bookingSchema.pre('save', async function(next) {
if (this.isNew) {
const slot = await mongoose.model('Slot').findById(this.slot);
if (slot.status !== 'available') {
throw new Error('Slot is not available for booking');
}
}
next();
});
// Check if slot time conflicts exist
slotSchema.methods.hasTimeConflict = async function() {
const conflicts = await this.constructor.find({
date: this.date,
venue: this.venue,
status: { $in: ['available', 'booked'] },
_id: { $ne: this._id },
$or: [
{
$and: [
{ startTime: { $lte: this.startTime } },
{ endTime: { $gt: this.startTime } }
]
},
{
$and: [
{ startTime: { $lt: this.endTime } },
{ endTime: { $gte: this.endTime } }
]
}
]
});
return conflicts.length > 0;
};