add changes

This commit is contained in:
AD2025
2025-12-26 23:56:32 +02:00
parent 410c3d725f
commit e7d26bc981
127 changed files with 36162 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
/**
* Add altering commands here.
*
* Example:
* await queryInterface.createTable('users', { id: Sequelize.INTEGER });
*/
},
async down (queryInterface, Sequelize) {
/**
* Add reverting commands here.
*
* Example:
* await queryInterface.dropTable('users');
*/
}
};

View File

@@ -0,0 +1,143 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('users', {
id: {
type: Sequelize.CHAR(36),
primaryKey: true,
allowNull: false,
defaultValue: Sequelize.UUIDV4,
comment: 'UUID primary key'
},
username: {
type: Sequelize.STRING(50),
allowNull: false,
unique: true,
comment: 'Unique username'
},
email: {
type: Sequelize.STRING(100),
allowNull: false,
unique: true,
comment: 'User email address'
},
password: {
type: Sequelize.STRING(255),
allowNull: false,
comment: 'Hashed password'
},
role: {
type: Sequelize.ENUM('admin', 'user'),
allowNull: false,
defaultValue: 'user',
comment: 'User role'
},
profile_image: {
type: Sequelize.STRING(255),
allowNull: true,
comment: 'Profile image URL'
},
is_active: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: true,
comment: 'Account active status'
},
// Statistics
total_quizzes: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Total number of quizzes taken'
},
quizzes_passed: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Number of quizzes passed'
},
total_questions_answered: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Total questions answered'
},
correct_answers: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Number of correct answers'
},
current_streak: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Current daily streak'
},
longest_streak: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Longest daily streak achieved'
},
// Timestamps
last_login: {
type: Sequelize.DATE,
allowNull: true,
comment: 'Last login timestamp'
},
last_quiz_date: {
type: Sequelize.DATE,
allowNull: true,
comment: 'Date of last quiz taken'
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
}
}, {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci'
});
// Add indexes
await queryInterface.addIndex('users', ['email'], {
name: 'idx_users_email',
unique: true
});
await queryInterface.addIndex('users', ['username'], {
name: 'idx_users_username',
unique: true
});
await queryInterface.addIndex('users', ['role'], {
name: 'idx_users_role'
});
await queryInterface.addIndex('users', ['is_active'], {
name: 'idx_users_is_active'
});
await queryInterface.addIndex('users', ['created_at'], {
name: 'idx_users_created_at'
});
console.log('✅ Users table created successfully with indexes');
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('users');
console.log('✅ Users table dropped successfully');
}
};

View File

@@ -0,0 +1,126 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('categories', {
id: {
type: Sequelize.CHAR(36),
primaryKey: true,
allowNull: false,
defaultValue: Sequelize.UUIDV4,
comment: 'UUID primary key'
},
name: {
type: Sequelize.STRING(100),
allowNull: false,
unique: true,
comment: 'Category name'
},
slug: {
type: Sequelize.STRING(100),
allowNull: false,
unique: true,
comment: 'URL-friendly slug'
},
description: {
type: Sequelize.TEXT,
allowNull: true,
comment: 'Category description'
},
icon: {
type: Sequelize.STRING(255),
allowNull: true,
comment: 'Icon URL or class'
},
color: {
type: Sequelize.STRING(20),
allowNull: true,
comment: 'Display color (hex or name)'
},
is_active: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: true,
comment: 'Category active status'
},
guest_accessible: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: 'Whether guests can access this category'
},
// Statistics
question_count: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Total number of questions in this category'
},
quiz_count: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Total number of quizzes taken in this category'
},
// Display order
display_order: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Display order (lower numbers first)'
},
// Timestamps
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
}
}, {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci'
});
// Add indexes
await queryInterface.addIndex('categories', ['name'], {
unique: true,
name: 'idx_categories_name'
});
await queryInterface.addIndex('categories', ['slug'], {
unique: true,
name: 'idx_categories_slug'
});
await queryInterface.addIndex('categories', ['is_active'], {
name: 'idx_categories_is_active'
});
await queryInterface.addIndex('categories', ['guest_accessible'], {
name: 'idx_categories_guest_accessible'
});
await queryInterface.addIndex('categories', ['display_order'], {
name: 'idx_categories_display_order'
});
await queryInterface.addIndex('categories', ['is_active', 'guest_accessible'], {
name: 'idx_categories_active_guest'
});
console.log('✅ Categories table created successfully with indexes');
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('categories');
console.log('✅ Categories table dropped successfully');
}
};

View File

@@ -0,0 +1,191 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
console.log('Creating questions table...');
await queryInterface.createTable('questions', {
id: {
type: Sequelize.CHAR(36),
primaryKey: true,
allowNull: false,
comment: 'UUID primary key'
},
category_id: {
type: Sequelize.CHAR(36),
allowNull: false,
references: {
model: 'categories',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
comment: 'Foreign key to categories table'
},
created_by: {
type: Sequelize.CHAR(36),
allowNull: true,
references: {
model: 'users',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL',
comment: 'User who created the question (admin)'
},
question_text: {
type: Sequelize.TEXT,
allowNull: false,
comment: 'The question text'
},
question_type: {
type: Sequelize.ENUM('multiple', 'trueFalse', 'written'),
allowNull: false,
defaultValue: 'multiple',
comment: 'Type of question'
},
options: {
type: Sequelize.JSON,
allowNull: true,
comment: 'Answer options for multiple choice (JSON array)'
},
correct_answer: {
type: Sequelize.STRING(255),
allowNull: false,
comment: 'Correct answer (index for multiple choice, true/false for boolean)'
},
explanation: {
type: Sequelize.TEXT,
allowNull: true,
comment: 'Explanation for the correct answer'
},
difficulty: {
type: Sequelize.ENUM('easy', 'medium', 'hard'),
allowNull: false,
defaultValue: 'medium',
comment: 'Question difficulty level'
},
points: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 10,
comment: 'Points awarded for correct answer'
},
time_limit: {
type: Sequelize.INTEGER,
allowNull: true,
comment: 'Time limit in seconds (optional)'
},
keywords: {
type: Sequelize.JSON,
allowNull: true,
comment: 'Search keywords (JSON array)'
},
tags: {
type: Sequelize.JSON,
allowNull: true,
comment: 'Tags for categorization (JSON array)'
},
visibility: {
type: Sequelize.ENUM('public', 'registered', 'premium'),
allowNull: false,
defaultValue: 'registered',
comment: 'Who can see this question'
},
guest_accessible: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: 'Whether guests can access this question'
},
is_active: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: true,
comment: 'Question active status'
},
times_attempted: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Number of times question was attempted'
},
times_correct: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Number of times answered correctly'
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
}
}, {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
engine: 'InnoDB'
});
// Add indexes
await queryInterface.addIndex('questions', ['category_id'], {
name: 'idx_questions_category_id'
});
await queryInterface.addIndex('questions', ['created_by'], {
name: 'idx_questions_created_by'
});
await queryInterface.addIndex('questions', ['question_type'], {
name: 'idx_questions_question_type'
});
await queryInterface.addIndex('questions', ['difficulty'], {
name: 'idx_questions_difficulty'
});
await queryInterface.addIndex('questions', ['visibility'], {
name: 'idx_questions_visibility'
});
await queryInterface.addIndex('questions', ['guest_accessible'], {
name: 'idx_questions_guest_accessible'
});
await queryInterface.addIndex('questions', ['is_active'], {
name: 'idx_questions_is_active'
});
await queryInterface.addIndex('questions', ['created_at'], {
name: 'idx_questions_created_at'
});
// Composite index for common query patterns
await queryInterface.addIndex('questions', ['category_id', 'is_active', 'difficulty'], {
name: 'idx_questions_category_active_difficulty'
});
await queryInterface.addIndex('questions', ['is_active', 'guest_accessible'], {
name: 'idx_questions_active_guest'
});
// Full-text search index
await queryInterface.sequelize.query(
'CREATE FULLTEXT INDEX idx_questions_fulltext ON questions(question_text, explanation)'
);
console.log('✅ Questions table created successfully with indexes and full-text search');
},
async down (queryInterface, Sequelize) {
console.log('Dropping questions table...');
await queryInterface.dropTable('questions');
console.log('✅ Questions table dropped successfully');
}
};

View File

@@ -0,0 +1,131 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
console.log('Creating guest_sessions table...');
await queryInterface.createTable('guest_sessions', {
id: {
type: Sequelize.CHAR(36),
primaryKey: true,
allowNull: false,
comment: 'UUID primary key'
},
guest_id: {
type: Sequelize.STRING(100),
allowNull: false,
unique: true,
comment: 'Unique guest identifier'
},
session_token: {
type: Sequelize.STRING(500),
allowNull: false,
unique: true,
comment: 'JWT session token'
},
device_id: {
type: Sequelize.STRING(255),
allowNull: true,
comment: 'Device identifier (optional)'
},
ip_address: {
type: Sequelize.STRING(45),
allowNull: true,
comment: 'IP address (supports IPv6)'
},
user_agent: {
type: Sequelize.TEXT,
allowNull: true,
comment: 'Browser user agent string'
},
quizzes_attempted: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Number of quizzes attempted by guest'
},
max_quizzes: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 3,
comment: 'Maximum quizzes allowed for this guest'
},
expires_at: {
type: Sequelize.DATE,
allowNull: false,
comment: 'Session expiration timestamp'
},
is_converted: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: 'Whether guest converted to registered user'
},
converted_user_id: {
type: Sequelize.CHAR(36),
allowNull: true,
references: {
model: 'users',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL',
comment: 'User ID if guest converted to registered user'
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
}
}, {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
engine: 'InnoDB'
});
// Add indexes
await queryInterface.addIndex('guest_sessions', ['guest_id'], {
unique: true,
name: 'idx_guest_sessions_guest_id'
});
await queryInterface.addIndex('guest_sessions', ['session_token'], {
unique: true,
name: 'idx_guest_sessions_session_token'
});
await queryInterface.addIndex('guest_sessions', ['expires_at'], {
name: 'idx_guest_sessions_expires_at'
});
await queryInterface.addIndex('guest_sessions', ['is_converted'], {
name: 'idx_guest_sessions_is_converted'
});
await queryInterface.addIndex('guest_sessions', ['converted_user_id'], {
name: 'idx_guest_sessions_converted_user_id'
});
await queryInterface.addIndex('guest_sessions', ['device_id'], {
name: 'idx_guest_sessions_device_id'
});
await queryInterface.addIndex('guest_sessions', ['created_at'], {
name: 'idx_guest_sessions_created_at'
});
console.log('✅ Guest sessions table created successfully with indexes');
},
async down (queryInterface, Sequelize) {
console.log('Dropping guest_sessions table...');
await queryInterface.dropTable('guest_sessions');
console.log('✅ Guest sessions table dropped successfully');
}
};

View File

@@ -0,0 +1,203 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('quiz_sessions', {
id: {
type: Sequelize.CHAR(36),
primaryKey: true,
allowNull: false,
comment: 'UUID primary key'
},
user_id: {
type: Sequelize.CHAR(36),
allowNull: true,
references: {
model: 'users',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL',
comment: 'Foreign key to users table (null for guest quizzes)'
},
guest_session_id: {
type: Sequelize.CHAR(36),
allowNull: true,
references: {
model: 'guest_sessions',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL',
comment: 'Foreign key to guest_sessions table (null for user quizzes)'
},
category_id: {
type: Sequelize.CHAR(36),
allowNull: false,
references: {
model: 'categories',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
comment: 'Foreign key to categories table'
},
quiz_type: {
type: Sequelize.ENUM('practice', 'timed', 'exam'),
allowNull: false,
defaultValue: 'practice',
comment: 'Type of quiz: practice (untimed), timed, or exam mode'
},
difficulty: {
type: Sequelize.ENUM('easy', 'medium', 'hard', 'mixed'),
allowNull: false,
defaultValue: 'mixed',
comment: 'Difficulty level of questions in the quiz'
},
total_questions: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false,
defaultValue: 10,
comment: 'Total number of questions in this quiz session'
},
questions_answered: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false,
defaultValue: 0,
comment: 'Number of questions answered so far'
},
correct_answers: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false,
defaultValue: 0,
comment: 'Number of correct answers'
},
score: {
type: Sequelize.DECIMAL(5, 2),
allowNull: false,
defaultValue: 0.00,
comment: 'Quiz score as percentage (0-100)'
},
total_points: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false,
defaultValue: 0,
comment: 'Total points earned in this quiz'
},
max_points: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false,
defaultValue: 0,
comment: 'Maximum possible points for this quiz'
},
time_limit: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: true,
comment: 'Time limit in seconds (null for untimed practice)'
},
time_spent: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false,
defaultValue: 0,
comment: 'Total time spent in seconds'
},
started_at: {
type: Sequelize.DATE,
allowNull: true,
comment: 'When the quiz was started'
},
completed_at: {
type: Sequelize.DATE,
allowNull: true,
comment: 'When the quiz was completed'
},
status: {
type: Sequelize.ENUM('not_started', 'in_progress', 'completed', 'abandoned', 'timed_out'),
allowNull: false,
defaultValue: 'not_started',
comment: 'Current status of the quiz session'
},
is_passed: {
type: Sequelize.BOOLEAN,
allowNull: true,
comment: 'Whether the quiz was passed (null if not completed)'
},
pass_percentage: {
type: Sequelize.DECIMAL(5, 2),
allowNull: false,
defaultValue: 70.00,
comment: 'Required percentage to pass (default 70%)'
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
comment: 'Record creation timestamp'
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
comment: 'Record last update timestamp'
}
}, {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
comment: 'Tracks individual quiz sessions for users and guests'
});
// Add indexes for better query performance
await queryInterface.addIndex('quiz_sessions', ['user_id'], {
name: 'idx_quiz_sessions_user_id'
});
await queryInterface.addIndex('quiz_sessions', ['guest_session_id'], {
name: 'idx_quiz_sessions_guest_session_id'
});
await queryInterface.addIndex('quiz_sessions', ['category_id'], {
name: 'idx_quiz_sessions_category_id'
});
await queryInterface.addIndex('quiz_sessions', ['status'], {
name: 'idx_quiz_sessions_status'
});
await queryInterface.addIndex('quiz_sessions', ['quiz_type'], {
name: 'idx_quiz_sessions_quiz_type'
});
await queryInterface.addIndex('quiz_sessions', ['started_at'], {
name: 'idx_quiz_sessions_started_at'
});
await queryInterface.addIndex('quiz_sessions', ['completed_at'], {
name: 'idx_quiz_sessions_completed_at'
});
await queryInterface.addIndex('quiz_sessions', ['created_at'], {
name: 'idx_quiz_sessions_created_at'
});
await queryInterface.addIndex('quiz_sessions', ['is_passed'], {
name: 'idx_quiz_sessions_is_passed'
});
// Composite index for common queries
await queryInterface.addIndex('quiz_sessions', ['user_id', 'status'], {
name: 'idx_quiz_sessions_user_status'
});
await queryInterface.addIndex('quiz_sessions', ['guest_session_id', 'status'], {
name: 'idx_quiz_sessions_guest_status'
});
console.log('✅ Quiz sessions table created successfully with 21 fields and 11 indexes');
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('quiz_sessions');
console.log('✅ Quiz sessions table dropped');
}
};

View File

@@ -0,0 +1,111 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('quiz_answers', {
id: {
type: Sequelize.CHAR(36),
primaryKey: true,
allowNull: false,
comment: 'UUID primary key'
},
quiz_session_id: {
type: Sequelize.CHAR(36),
allowNull: false,
references: {
model: 'quiz_sessions',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
comment: 'Foreign key to quiz_sessions table'
},
question_id: {
type: Sequelize.CHAR(36),
allowNull: false,
references: {
model: 'questions',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
comment: 'Foreign key to questions table'
},
selected_option: {
type: Sequelize.STRING(255),
allowNull: false,
comment: 'The option selected by the user'
},
is_correct: {
type: Sequelize.BOOLEAN,
allowNull: false,
comment: 'Whether the selected answer was correct'
},
points_earned: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false,
defaultValue: 0,
comment: 'Points earned for this answer'
},
time_taken: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false,
defaultValue: 0,
comment: 'Time taken to answer in seconds'
},
answered_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
comment: 'When the question was answered'
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
comment: 'Record creation timestamp'
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
comment: 'Record last update timestamp'
}
}, {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
comment: 'Stores individual answers given during quiz sessions'
});
// Add indexes
await queryInterface.addIndex('quiz_answers', ['quiz_session_id'], {
name: 'idx_quiz_answers_session_id'
});
await queryInterface.addIndex('quiz_answers', ['question_id'], {
name: 'idx_quiz_answers_question_id'
});
await queryInterface.addIndex('quiz_answers', ['is_correct'], {
name: 'idx_quiz_answers_is_correct'
});
await queryInterface.addIndex('quiz_answers', ['answered_at'], {
name: 'idx_quiz_answers_answered_at'
});
// Composite index for session + question (unique constraint)
await queryInterface.addIndex('quiz_answers', ['quiz_session_id', 'question_id'], {
name: 'idx_quiz_answers_session_question',
unique: true
});
console.log('✅ Quiz answers table created successfully with 9 fields and 5 indexes');
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('quiz_answers');
console.log('✅ Quiz answers table dropped');
}
};

View File

@@ -0,0 +1,84 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('quiz_session_questions', {
id: {
type: Sequelize.CHAR(36),
primaryKey: true,
allowNull: false,
comment: 'UUID primary key'
},
quiz_session_id: {
type: Sequelize.CHAR(36),
allowNull: false,
references: {
model: 'quiz_sessions',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
comment: 'Foreign key to quiz_sessions table'
},
question_id: {
type: Sequelize.CHAR(36),
allowNull: false,
references: {
model: 'questions',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
comment: 'Foreign key to questions table'
},
question_order: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false,
comment: 'Order of question in the quiz (1-based)'
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
comment: 'Record creation timestamp'
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
comment: 'Record last update timestamp'
}
}, {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
comment: 'Junction table linking quiz sessions with questions'
});
// Add indexes
await queryInterface.addIndex('quiz_session_questions', ['quiz_session_id'], {
name: 'idx_qsq_session_id'
});
await queryInterface.addIndex('quiz_session_questions', ['question_id'], {
name: 'idx_qsq_question_id'
});
await queryInterface.addIndex('quiz_session_questions', ['question_order'], {
name: 'idx_qsq_question_order'
});
// Unique composite index to prevent duplicate questions in same session
await queryInterface.addIndex('quiz_session_questions', ['quiz_session_id', 'question_id'], {
name: 'idx_qsq_session_question',
unique: true
});
console.log('✅ Quiz session questions junction table created with 5 fields and 4 indexes');
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('quiz_session_questions');
console.log('✅ Quiz session questions table dropped');
}
};

View File

@@ -0,0 +1,84 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('user_bookmarks', {
id: {
type: Sequelize.CHAR(36),
primaryKey: true,
allowNull: false,
comment: 'UUID primary key'
},
user_id: {
type: Sequelize.CHAR(36),
allowNull: false,
references: {
model: 'users',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
comment: 'Foreign key to users table'
},
question_id: {
type: Sequelize.CHAR(36),
allowNull: false,
references: {
model: 'questions',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
comment: 'Foreign key to questions table'
},
notes: {
type: Sequelize.TEXT,
allowNull: true,
comment: 'Optional user notes about the bookmarked question'
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
comment: 'When the bookmark was created'
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
comment: 'Record last update timestamp'
}
}, {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
comment: 'Junction table for users bookmarking questions'
});
// Add indexes
await queryInterface.addIndex('user_bookmarks', ['user_id'], {
name: 'idx_user_bookmarks_user_id'
});
await queryInterface.addIndex('user_bookmarks', ['question_id'], {
name: 'idx_user_bookmarks_question_id'
});
await queryInterface.addIndex('user_bookmarks', ['created_at'], {
name: 'idx_user_bookmarks_created_at'
});
// Unique composite index to prevent duplicate bookmarks
await queryInterface.addIndex('user_bookmarks', ['user_id', 'question_id'], {
name: 'idx_user_bookmarks_user_question',
unique: true
});
console.log('✅ User bookmarks table created with 5 fields and 4 indexes');
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('user_bookmarks');
console.log('✅ User bookmarks table dropped');
}
};

View File

@@ -0,0 +1,122 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('achievements', {
id: {
type: Sequelize.CHAR(36),
primaryKey: true,
allowNull: false,
comment: 'UUID primary key'
},
name: {
type: Sequelize.STRING(100),
allowNull: false,
unique: true,
comment: 'Unique name of the achievement'
},
slug: {
type: Sequelize.STRING(100),
allowNull: false,
unique: true,
comment: 'URL-friendly slug'
},
description: {
type: Sequelize.TEXT,
allowNull: false,
comment: 'Description of the achievement'
},
icon: {
type: Sequelize.STRING(50),
allowNull: true,
comment: 'Icon identifier (e.g., emoji or icon class)'
},
badge_color: {
type: Sequelize.STRING(20),
allowNull: true,
defaultValue: '#FFD700',
comment: 'Hex color code for the badge'
},
category: {
type: Sequelize.ENUM('quiz', 'streak', 'score', 'speed', 'milestone', 'special'),
allowNull: false,
defaultValue: 'milestone',
comment: 'Category of achievement'
},
requirement_type: {
type: Sequelize.ENUM('quizzes_completed', 'quizzes_passed', 'perfect_score', 'streak_days', 'total_questions', 'category_master', 'speed_demon', 'early_bird'),
allowNull: false,
comment: 'Type of requirement to earn the achievement'
},
requirement_value: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false,
comment: 'Value needed to satisfy requirement (e.g., 10 for "10 quizzes")'
},
points: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false,
defaultValue: 10,
comment: 'Points awarded when achievement is earned'
},
is_active: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: true,
comment: 'Whether this achievement is currently available'
},
display_order: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false,
defaultValue: 0,
comment: 'Display order in achievement list'
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
comment: 'Record creation timestamp'
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
comment: 'Record last update timestamp'
}
}, {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
comment: 'Defines available achievements users can earn'
});
// Add indexes
await queryInterface.addIndex('achievements', ['slug'], {
name: 'idx_achievements_slug',
unique: true
});
await queryInterface.addIndex('achievements', ['category'], {
name: 'idx_achievements_category'
});
await queryInterface.addIndex('achievements', ['requirement_type'], {
name: 'idx_achievements_requirement_type'
});
await queryInterface.addIndex('achievements', ['is_active'], {
name: 'idx_achievements_is_active'
});
await queryInterface.addIndex('achievements', ['display_order'], {
name: 'idx_achievements_display_order'
});
console.log('✅ Achievements table created with 13 fields and 5 indexes');
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('achievements');
console.log('✅ Achievements table dropped');
}
};

View File

@@ -0,0 +1,95 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('user_achievements', {
id: {
type: Sequelize.CHAR(36),
primaryKey: true,
allowNull: false,
comment: 'UUID primary key'
},
user_id: {
type: Sequelize.CHAR(36),
allowNull: false,
references: {
model: 'users',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
comment: 'Foreign key to users table'
},
achievement_id: {
type: Sequelize.CHAR(36),
allowNull: false,
references: {
model: 'achievements',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
comment: 'Foreign key to achievements table'
},
earned_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
comment: 'When the achievement was earned'
},
notified: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: 'Whether user has been notified about this achievement'
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
comment: 'Record creation timestamp'
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
comment: 'Record last update timestamp'
}
}, {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
comment: 'Junction table tracking achievements earned by users'
});
// Add indexes
await queryInterface.addIndex('user_achievements', ['user_id'], {
name: 'idx_user_achievements_user_id'
});
await queryInterface.addIndex('user_achievements', ['achievement_id'], {
name: 'idx_user_achievements_achievement_id'
});
await queryInterface.addIndex('user_achievements', ['earned_at'], {
name: 'idx_user_achievements_earned_at'
});
await queryInterface.addIndex('user_achievements', ['notified'], {
name: 'idx_user_achievements_notified'
});
// Unique composite index to prevent duplicate achievements
await queryInterface.addIndex('user_achievements', ['user_id', 'achievement_id'], {
name: 'idx_user_achievements_user_achievement',
unique: true
});
console.log('✅ User achievements table created with 6 fields and 5 indexes');
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('user_achievements');
console.log('✅ User achievements table dropped');
}
};

View File

@@ -0,0 +1,105 @@
/**
* Migration: Add Database Indexes for Performance Optimization
*
* This migration adds indexes to improve query performance for:
* - QuizSession: userId, guestSessionId, categoryId, status, createdAt
* - QuizSessionQuestion: quizSessionId, questionId
*
* Note: Other models (User, Question, Category, GuestSession, QuizAnswer, UserBookmark)
* already have indexes defined in their models.
*/
module.exports = {
up: async (queryInterface, Sequelize) => {
console.log('Adding performance indexes...');
try {
// QuizSession indexes
await queryInterface.addIndex('quiz_sessions', ['user_id'], {
name: 'idx_quiz_sessions_user_id'
});
await queryInterface.addIndex('quiz_sessions', ['guest_session_id'], {
name: 'idx_quiz_sessions_guest_session_id'
});
await queryInterface.addIndex('quiz_sessions', ['category_id'], {
name: 'idx_quiz_sessions_category_id'
});
await queryInterface.addIndex('quiz_sessions', ['status'], {
name: 'idx_quiz_sessions_status'
});
await queryInterface.addIndex('quiz_sessions', ['created_at'], {
name: 'idx_quiz_sessions_created_at'
});
// Composite indexes for common queries
await queryInterface.addIndex('quiz_sessions', ['user_id', 'created_at'], {
name: 'idx_quiz_sessions_user_created'
});
await queryInterface.addIndex('quiz_sessions', ['guest_session_id', 'created_at'], {
name: 'idx_quiz_sessions_guest_created'
});
await queryInterface.addIndex('quiz_sessions', ['category_id', 'status'], {
name: 'idx_quiz_sessions_category_status'
});
// QuizSessionQuestion indexes
await queryInterface.addIndex('quiz_session_questions', ['quiz_session_id'], {
name: 'idx_quiz_session_questions_session_id'
});
await queryInterface.addIndex('quiz_session_questions', ['question_id'], {
name: 'idx_quiz_session_questions_question_id'
});
await queryInterface.addIndex('quiz_session_questions', ['quiz_session_id', 'question_order'], {
name: 'idx_quiz_session_questions_session_order'
});
// Unique constraint to prevent duplicate questions in same session
await queryInterface.addIndex('quiz_session_questions', ['quiz_session_id', 'question_id'], {
name: 'idx_quiz_session_questions_session_question_unique',
unique: true
});
console.log('✅ Performance indexes added successfully');
} catch (error) {
console.error('❌ Error adding indexes:', error);
throw error;
}
},
down: async (queryInterface, Sequelize) => {
console.log('Removing performance indexes...');
try {
// Remove QuizSession indexes
await queryInterface.removeIndex('quiz_sessions', 'idx_quiz_sessions_user_id');
await queryInterface.removeIndex('quiz_sessions', 'idx_quiz_sessions_guest_session_id');
await queryInterface.removeIndex('quiz_sessions', 'idx_quiz_sessions_category_id');
await queryInterface.removeIndex('quiz_sessions', 'idx_quiz_sessions_status');
await queryInterface.removeIndex('quiz_sessions', 'idx_quiz_sessions_created_at');
await queryInterface.removeIndex('quiz_sessions', 'idx_quiz_sessions_user_created');
await queryInterface.removeIndex('quiz_sessions', 'idx_quiz_sessions_guest_created');
await queryInterface.removeIndex('quiz_sessions', 'idx_quiz_sessions_category_status');
// Remove QuizSessionQuestion indexes
await queryInterface.removeIndex('quiz_session_questions', 'idx_quiz_session_questions_session_id');
await queryInterface.removeIndex('quiz_session_questions', 'idx_quiz_session_questions_question_id');
await queryInterface.removeIndex('quiz_session_questions', 'idx_quiz_session_questions_session_order');
await queryInterface.removeIndex('quiz_session_questions', 'idx_quiz_session_questions_session_question_unique');
console.log('✅ Performance indexes removed successfully');
} catch (error) {
console.error('❌ Error removing indexes:', error);
throw error;
}
}
};

View File

@@ -0,0 +1,61 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
console.log('Creating guest_settings table...');
await queryInterface.createTable('guest_settings', {
id: {
type: Sequelize.CHAR(36),
primaryKey: true,
allowNull: false,
comment: 'UUID primary key'
},
max_quizzes: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 3,
comment: 'Maximum number of quizzes a guest can take'
},
expiry_hours: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 24,
comment: 'Guest session expiry time in hours'
},
public_categories: {
type: Sequelize.JSON,
allowNull: false,
defaultValue: '[]',
comment: 'Array of category UUIDs accessible to guests'
},
feature_restrictions: {
type: Sequelize.JSON,
allowNull: false,
defaultValue: '{"allowBookmarks":false,"allowReview":true,"allowPracticeMode":true,"allowTimedMode":false,"allowExamMode":false}',
comment: 'Feature restrictions for guest users'
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
}
}, {
comment: 'System-wide guest user settings'
});
console.log('✅ guest_settings table created successfully');
},
async down (queryInterface, Sequelize) {
console.log('Dropping guest_settings table...');
await queryInterface.dropTable('guest_settings');
console.log('✅ guest_settings table dropped successfully');
}
};