642 lines
14 KiB
Markdown
642 lines
14 KiB
Markdown
# Sequelize Quick Reference Guide
|
|
|
|
## Common Operations for Interview Quiz App
|
|
|
|
### 1. Database Connection
|
|
|
|
```javascript
|
|
// config/database.js
|
|
const { Sequelize } = require('sequelize');
|
|
|
|
const sequelize = new Sequelize(
|
|
process.env.DB_NAME,
|
|
process.env.DB_USER,
|
|
process.env.DB_PASSWORD,
|
|
{
|
|
host: process.env.DB_HOST,
|
|
dialect: 'mysql',
|
|
pool: { max: 10, min: 0, acquire: 30000, idle: 10000 }
|
|
}
|
|
);
|
|
|
|
// Test connection
|
|
sequelize.authenticate()
|
|
.then(() => console.log('MySQL connected'))
|
|
.catch(err => console.error('Unable to connect:', err));
|
|
|
|
module.exports = sequelize;
|
|
```
|
|
|
|
---
|
|
|
|
### 2. User Operations
|
|
|
|
#### Register New User
|
|
```javascript
|
|
const bcrypt = require('bcrypt');
|
|
const { User } = require('../models');
|
|
|
|
const registerUser = async (userData) => {
|
|
const hashedPassword = await bcrypt.hash(userData.password, 10);
|
|
|
|
const user = await User.create({
|
|
username: userData.username,
|
|
email: userData.email,
|
|
password: hashedPassword,
|
|
role: 'user'
|
|
});
|
|
|
|
return user;
|
|
};
|
|
```
|
|
|
|
#### Login User
|
|
```javascript
|
|
const loginUser = async (email, password) => {
|
|
const user = await User.findOne({
|
|
where: { email },
|
|
attributes: ['id', 'username', 'email', 'password', 'role']
|
|
});
|
|
|
|
if (!user) {
|
|
throw new Error('User not found');
|
|
}
|
|
|
|
const isValid = await bcrypt.compare(password, user.password);
|
|
if (!isValid) {
|
|
throw new Error('Invalid credentials');
|
|
}
|
|
|
|
return user;
|
|
};
|
|
```
|
|
|
|
#### Get User Dashboard
|
|
```javascript
|
|
const getUserDashboard = async (userId) => {
|
|
const user = await User.findByPk(userId, {
|
|
attributes: [
|
|
'id', 'username', 'email',
|
|
'totalQuizzes', 'totalQuestions', 'correctAnswers', 'streak'
|
|
],
|
|
include: [
|
|
{
|
|
model: QuizSession,
|
|
where: { status: 'completed' },
|
|
required: false,
|
|
limit: 10,
|
|
order: [['completedAt', 'DESC']],
|
|
include: [{ model: Category, attributes: ['name'] }]
|
|
}
|
|
]
|
|
});
|
|
|
|
return user;
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
### 3. Category Operations
|
|
|
|
#### Get All Categories
|
|
```javascript
|
|
const getCategories = async () => {
|
|
const categories = await Category.findAll({
|
|
where: { isActive: true },
|
|
attributes: {
|
|
include: [
|
|
[
|
|
sequelize.literal(`(
|
|
SELECT COUNT(*)
|
|
FROM questions
|
|
WHERE questions.category_id = categories.id
|
|
AND questions.is_active = true
|
|
)`),
|
|
'questionCount'
|
|
]
|
|
]
|
|
}
|
|
});
|
|
|
|
return categories;
|
|
};
|
|
```
|
|
|
|
#### Get Guest-Accessible Categories
|
|
```javascript
|
|
const getGuestCategories = async () => {
|
|
const categories = await Category.findAll({
|
|
where: {
|
|
isActive: true,
|
|
guestAccessible: true
|
|
},
|
|
attributes: ['id', 'name', 'description', 'icon', 'publicQuestionCount']
|
|
});
|
|
|
|
return categories;
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
### 4. Question Operations
|
|
|
|
#### Create Question (Admin)
|
|
```javascript
|
|
const createQuestion = async (questionData) => {
|
|
const question = await Question.create({
|
|
question: questionData.question,
|
|
type: questionData.type,
|
|
categoryId: questionData.categoryId,
|
|
difficulty: questionData.difficulty,
|
|
options: JSON.stringify(questionData.options), // Store as JSON
|
|
correctAnswer: questionData.correctAnswer,
|
|
explanation: questionData.explanation,
|
|
keywords: JSON.stringify(questionData.keywords),
|
|
tags: JSON.stringify(questionData.tags),
|
|
visibility: questionData.visibility || 'registered',
|
|
isGuestAccessible: questionData.isGuestAccessible || false,
|
|
createdBy: questionData.userId
|
|
});
|
|
|
|
// Update category question count
|
|
await Category.increment('questionCount', {
|
|
where: { id: questionData.categoryId }
|
|
});
|
|
|
|
return question;
|
|
};
|
|
```
|
|
|
|
#### Get Questions by Category
|
|
```javascript
|
|
const getQuestionsByCategory = async (categoryId, options = {}) => {
|
|
const { difficulty, limit = 10, visibility = 'registered' } = options;
|
|
|
|
const where = {
|
|
categoryId,
|
|
isActive: true,
|
|
visibility: {
|
|
[Op.in]: visibility === 'guest' ? ['public'] : ['public', 'registered', 'premium']
|
|
}
|
|
};
|
|
|
|
if (difficulty) {
|
|
where.difficulty = difficulty;
|
|
}
|
|
|
|
const questions = await Question.findAll({
|
|
where,
|
|
order: sequelize.random(),
|
|
limit,
|
|
include: [{ model: Category, attributes: ['name'] }]
|
|
});
|
|
|
|
return questions;
|
|
};
|
|
```
|
|
|
|
#### Search Questions (Full-Text)
|
|
```javascript
|
|
const searchQuestions = async (searchTerm) => {
|
|
const questions = await Question.findAll({
|
|
where: {
|
|
[Op.or]: [
|
|
sequelize.literal(`MATCH(question, explanation) AGAINST('${searchTerm}' IN NATURAL LANGUAGE MODE)`)
|
|
],
|
|
isActive: true
|
|
},
|
|
limit: 50
|
|
});
|
|
|
|
return questions;
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
### 5. Quiz Session Operations
|
|
|
|
#### Start Quiz Session
|
|
```javascript
|
|
const startQuizSession = async (sessionData) => {
|
|
const { userId, guestSessionId, categoryId, questionCount, difficulty } = sessionData;
|
|
|
|
// Create quiz session
|
|
const quizSession = await QuizSession.create({
|
|
userId: userId || null,
|
|
guestSessionId: guestSessionId || null,
|
|
isGuestSession: !userId,
|
|
categoryId,
|
|
totalQuestions: questionCount,
|
|
status: 'in-progress',
|
|
startTime: new Date()
|
|
});
|
|
|
|
// Get random questions
|
|
const questions = await Question.findAll({
|
|
where: {
|
|
categoryId,
|
|
difficulty: difficulty || { [Op.in]: ['easy', 'medium', 'hard'] },
|
|
isActive: true
|
|
},
|
|
order: sequelize.random(),
|
|
limit: questionCount
|
|
});
|
|
|
|
// Create junction entries
|
|
const sessionQuestions = questions.map((q, index) => ({
|
|
quizSessionId: quizSession.id,
|
|
questionId: q.id,
|
|
questionOrder: index + 1
|
|
}));
|
|
|
|
await sequelize.models.QuizSessionQuestion.bulkCreate(sessionQuestions);
|
|
|
|
return { quizSession, questions };
|
|
};
|
|
```
|
|
|
|
#### Submit Answer
|
|
```javascript
|
|
const submitAnswer = async (answerData) => {
|
|
const { quizSessionId, questionId, userAnswer } = answerData;
|
|
|
|
// Get question
|
|
const question = await Question.findByPk(questionId);
|
|
|
|
// Check if correct
|
|
const isCorrect = question.correctAnswer === userAnswer;
|
|
|
|
// Save answer
|
|
const answer = await QuizAnswer.create({
|
|
quizSessionId,
|
|
questionId,
|
|
userAnswer,
|
|
isCorrect,
|
|
timeSpent: answerData.timeSpent || 0
|
|
});
|
|
|
|
// Update quiz session score
|
|
if (isCorrect) {
|
|
await QuizSession.increment('score', { where: { id: quizSessionId } });
|
|
}
|
|
|
|
// Update question statistics
|
|
await Question.increment('timesAttempted', { where: { id: questionId } });
|
|
|
|
return {
|
|
isCorrect,
|
|
correctAnswer: question.correctAnswer,
|
|
explanation: question.explanation
|
|
};
|
|
};
|
|
```
|
|
|
|
#### Complete Quiz Session
|
|
```javascript
|
|
const completeQuizSession = async (sessionId) => {
|
|
const session = await QuizSession.findByPk(sessionId, {
|
|
include: [
|
|
{
|
|
model: QuizAnswer,
|
|
include: [{ model: Question, attributes: ['id', 'question'] }]
|
|
}
|
|
]
|
|
});
|
|
|
|
// Calculate results
|
|
const correctAnswers = session.QuizAnswers.filter(a => a.isCorrect).length;
|
|
const percentage = (correctAnswers / session.totalQuestions) * 100;
|
|
const timeTaken = Math.floor((new Date() - session.startTime) / 1000); // seconds
|
|
|
|
// Update session
|
|
await session.update({
|
|
status: 'completed',
|
|
endTime: new Date(),
|
|
completedAt: new Date()
|
|
});
|
|
|
|
// Update user stats if not guest
|
|
if (session.userId) {
|
|
await User.increment({
|
|
totalQuizzes: 1,
|
|
totalQuestions: session.totalQuestions,
|
|
correctAnswers: correctAnswers
|
|
}, { where: { id: session.userId } });
|
|
}
|
|
|
|
return {
|
|
score: session.score,
|
|
totalQuestions: session.totalQuestions,
|
|
percentage,
|
|
timeTaken,
|
|
correctAnswers,
|
|
incorrectAnswers: session.totalQuestions - correctAnswers,
|
|
results: session.QuizAnswers
|
|
};
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
### 6. Guest Session Operations
|
|
|
|
#### Create Guest Session
|
|
```javascript
|
|
const createGuestSession = async (deviceId, ipAddress, userAgent) => {
|
|
const guestId = `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
const sessionToken = jwt.sign({ guestId }, process.env.JWT_SECRET, { expiresIn: '24h' });
|
|
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours
|
|
|
|
const guestSession = await GuestSession.create({
|
|
guestId,
|
|
deviceId,
|
|
sessionToken,
|
|
quizzesAttempted: 0,
|
|
maxQuizzes: 3,
|
|
expiresAt,
|
|
ipAddress,
|
|
userAgent
|
|
});
|
|
|
|
return guestSession;
|
|
};
|
|
```
|
|
|
|
#### Check Guest Quiz Limit
|
|
```javascript
|
|
const checkGuestQuizLimit = async (guestSessionId) => {
|
|
const session = await GuestSession.findByPk(guestSessionId);
|
|
|
|
if (!session) {
|
|
throw new Error('Guest session not found');
|
|
}
|
|
|
|
if (new Date() > session.expiresAt) {
|
|
throw new Error('Guest session expired');
|
|
}
|
|
|
|
const remainingQuizzes = session.maxQuizzes - session.quizzesAttempted;
|
|
|
|
if (remainingQuizzes <= 0) {
|
|
throw new Error('Guest quiz limit reached');
|
|
}
|
|
|
|
return {
|
|
remainingQuizzes,
|
|
resetTime: session.expiresAt
|
|
};
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
### 7. Bookmark Operations
|
|
|
|
#### Add Bookmark
|
|
```javascript
|
|
const addBookmark = async (userId, questionId) => {
|
|
const bookmark = await sequelize.models.UserBookmark.create({
|
|
userId,
|
|
questionId
|
|
});
|
|
|
|
return bookmark;
|
|
};
|
|
```
|
|
|
|
#### Get User Bookmarks
|
|
```javascript
|
|
const getUserBookmarks = async (userId) => {
|
|
const user = await User.findByPk(userId, {
|
|
include: [{
|
|
model: Question,
|
|
as: 'bookmarks',
|
|
through: { attributes: ['bookmarkedAt'] },
|
|
include: [{ model: Category, attributes: ['name'] }]
|
|
}]
|
|
});
|
|
|
|
return user.bookmarks;
|
|
};
|
|
```
|
|
|
|
#### Remove Bookmark
|
|
```javascript
|
|
const removeBookmark = async (userId, questionId) => {
|
|
await sequelize.models.UserBookmark.destroy({
|
|
where: { userId, questionId }
|
|
});
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
### 8. Admin Operations
|
|
|
|
#### Get System Statistics
|
|
```javascript
|
|
const getSystemStats = async () => {
|
|
const [totalUsers, activeUsers, totalQuizzes] = await Promise.all([
|
|
User.count(),
|
|
User.count({
|
|
where: {
|
|
lastLogin: { [Op.gte]: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }
|
|
}
|
|
}),
|
|
QuizSession.count({ where: { status: 'completed' } })
|
|
]);
|
|
|
|
const popularCategories = await Category.findAll({
|
|
attributes: [
|
|
'id', 'name',
|
|
[sequelize.fn('COUNT', sequelize.col('QuizSessions.id')), 'quizCount']
|
|
],
|
|
include: [{
|
|
model: QuizSession,
|
|
attributes: [],
|
|
required: false
|
|
}],
|
|
group: ['Category.id'],
|
|
order: [[sequelize.literal('quizCount'), 'DESC']],
|
|
limit: 5
|
|
});
|
|
|
|
const avgScore = await QuizSession.findOne({
|
|
attributes: [[sequelize.fn('AVG', sequelize.col('score')), 'avgScore']],
|
|
where: { status: 'completed' }
|
|
});
|
|
|
|
return {
|
|
totalUsers,
|
|
activeUsers,
|
|
totalQuizzes,
|
|
popularCategories,
|
|
averageScore: avgScore.dataValues.avgScore || 0
|
|
};
|
|
};
|
|
```
|
|
|
|
#### Update Guest Settings
|
|
```javascript
|
|
const updateGuestSettings = async (settings, adminUserId) => {
|
|
const guestSettings = await GuestSettings.findOne();
|
|
|
|
if (guestSettings) {
|
|
await guestSettings.update({
|
|
...settings,
|
|
updatedBy: adminUserId
|
|
});
|
|
} else {
|
|
await GuestSettings.create({
|
|
...settings,
|
|
updatedBy: adminUserId
|
|
});
|
|
}
|
|
|
|
return guestSettings;
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
### 9. Transaction Example
|
|
|
|
```javascript
|
|
const { Op } = require('sequelize');
|
|
|
|
const convertGuestToUser = async (guestSessionId, userData) => {
|
|
const t = await sequelize.transaction();
|
|
|
|
try {
|
|
// Create user
|
|
const user = await User.create(userData, { transaction: t });
|
|
|
|
// Get guest session
|
|
const guestSession = await GuestSession.findByPk(guestSessionId, { transaction: t });
|
|
|
|
// Migrate quiz sessions
|
|
await QuizSession.update(
|
|
{ userId: user.id, isGuestSession: false },
|
|
{ where: { guestSessionId }, transaction: t }
|
|
);
|
|
|
|
// Calculate stats
|
|
const stats = await QuizSession.findAll({
|
|
where: { userId: user.id },
|
|
attributes: [
|
|
[sequelize.fn('COUNT', sequelize.col('id')), 'totalQuizzes'],
|
|
[sequelize.fn('SUM', sequelize.col('score')), 'totalScore']
|
|
],
|
|
transaction: t
|
|
});
|
|
|
|
// Update user stats
|
|
await user.update({
|
|
totalQuizzes: stats[0].dataValues.totalQuizzes || 0
|
|
}, { transaction: t });
|
|
|
|
// Delete guest session
|
|
await guestSession.destroy({ transaction: t });
|
|
|
|
await t.commit();
|
|
return user;
|
|
} catch (error) {
|
|
await t.rollback();
|
|
throw error;
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
### 10. Common Query Operators
|
|
|
|
```javascript
|
|
const { Op } = require('sequelize');
|
|
|
|
// Examples of common operators
|
|
const examples = {
|
|
// Equals
|
|
{ status: 'completed' },
|
|
|
|
// Not equals
|
|
{ status: { [Op.ne]: 'abandoned' } },
|
|
|
|
// Greater than / Less than
|
|
{ score: { [Op.gte]: 80 } },
|
|
{ createdAt: { [Op.lte]: new Date() } },
|
|
|
|
// Between
|
|
{ score: { [Op.between]: [50, 100] } },
|
|
|
|
// In array
|
|
{ difficulty: { [Op.in]: ['easy', 'medium'] } },
|
|
|
|
// Like (case-sensitive)
|
|
{ question: { [Op.like]: '%javascript%' } },
|
|
|
|
// Not null
|
|
{ userId: { [Op.not]: null } },
|
|
|
|
// OR condition
|
|
{
|
|
[Op.or]: [
|
|
{ visibility: 'public' },
|
|
{ isGuestAccessible: true }
|
|
]
|
|
},
|
|
|
|
// AND condition
|
|
{
|
|
[Op.and]: [
|
|
{ isActive: true },
|
|
{ difficulty: 'hard' }
|
|
]
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Error Handling
|
|
|
|
```javascript
|
|
const handleSequelizeError = (error) => {
|
|
if (error.name === 'SequelizeUniqueConstraintError') {
|
|
return { status: 400, message: 'Record already exists' };
|
|
}
|
|
|
|
if (error.name === 'SequelizeValidationError') {
|
|
return { status: 400, message: error.errors.map(e => e.message).join(', ') };
|
|
}
|
|
|
|
if (error.name === 'SequelizeForeignKeyConstraintError') {
|
|
return { status: 400, message: 'Invalid foreign key reference' };
|
|
}
|
|
|
|
return { status: 500, message: 'Database error' };
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
1. **Always use parameterized queries** (Sequelize does this by default)
|
|
2. **Use transactions for multi-step operations**
|
|
3. **Index frequently queried columns**
|
|
4. **Use eager loading to avoid N+1 queries**
|
|
5. **Limit result sets with pagination**
|
|
6. **Use connection pooling**
|
|
7. **Handle errors gracefully**
|
|
8. **Log slow queries in development**
|
|
9. **Use prepared statements**
|
|
10. **Validate input before database operations**
|
|
|
|
---
|
|
|
|
Happy coding! 🚀
|