Files
Tasks/SEQUELIZE_QUICK_REFERENCE.md
2025-11-11 00:25:50 +02:00

14 KiB

Sequelize Quick Reference Guide

Common Operations for Interview Quiz App

1. Database Connection

// 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

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

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

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

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

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)

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

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)

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

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

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

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

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

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

const addBookmark = async (userId, questionId) => {
  const bookmark = await sequelize.models.UserBookmark.create({
    userId,
    questionId
  });
  
  return bookmark;
};

Get User Bookmarks

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

const removeBookmark = async (userId, questionId) => {
  await sequelize.models.UserBookmark.destroy({
    where: { userId, questionId }
  });
};

8. Admin Operations

Get System Statistics

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

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

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

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

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! 🚀