673 lines
16 KiB
Markdown
673 lines
16 KiB
Markdown
# Sample Sequelize Migration Files
|
|
|
|
## Migration 1: Create Users Table
|
|
|
|
**File**: `migrations/20250101000001-create-users.js`
|
|
|
|
```javascript
|
|
'use strict';
|
|
|
|
module.exports = {
|
|
up: async (queryInterface, Sequelize) => {
|
|
await queryInterface.createTable('users', {
|
|
id: {
|
|
type: Sequelize.UUID,
|
|
defaultValue: Sequelize.UUIDV4,
|
|
primaryKey: true
|
|
},
|
|
username: {
|
|
type: Sequelize.STRING(50),
|
|
unique: true,
|
|
allowNull: false
|
|
},
|
|
email: {
|
|
type: Sequelize.STRING(255),
|
|
unique: true,
|
|
allowNull: false
|
|
},
|
|
password: {
|
|
type: Sequelize.STRING(255),
|
|
allowNull: false
|
|
},
|
|
role: {
|
|
type: Sequelize.ENUM('user', 'admin'),
|
|
defaultValue: 'user'
|
|
},
|
|
profile_image: {
|
|
type: Sequelize.STRING(500)
|
|
},
|
|
created_at: {
|
|
type: Sequelize.DATE,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
},
|
|
updated_at: {
|
|
type: Sequelize.DATE,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
|
|
},
|
|
last_login: {
|
|
type: Sequelize.DATE,
|
|
allowNull: true
|
|
},
|
|
total_quizzes: {
|
|
type: Sequelize.INTEGER,
|
|
defaultValue: 0
|
|
},
|
|
total_questions: {
|
|
type: Sequelize.INTEGER,
|
|
defaultValue: 0
|
|
},
|
|
correct_answers: {
|
|
type: Sequelize.INTEGER,
|
|
defaultValue: 0
|
|
},
|
|
streak: {
|
|
type: Sequelize.INTEGER,
|
|
defaultValue: 0
|
|
}
|
|
});
|
|
|
|
// Add indexes
|
|
await queryInterface.addIndex('users', ['email']);
|
|
await queryInterface.addIndex('users', ['username']);
|
|
await queryInterface.addIndex('users', ['role']);
|
|
},
|
|
|
|
down: async (queryInterface, Sequelize) => {
|
|
await queryInterface.dropTable('users');
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Migration 2: Create Categories Table
|
|
|
|
**File**: `migrations/20250101000002-create-categories.js`
|
|
|
|
```javascript
|
|
'use strict';
|
|
|
|
module.exports = {
|
|
up: async (queryInterface, Sequelize) => {
|
|
await queryInterface.createTable('categories', {
|
|
id: {
|
|
type: Sequelize.UUID,
|
|
defaultValue: Sequelize.UUIDV4,
|
|
primaryKey: true
|
|
},
|
|
name: {
|
|
type: Sequelize.STRING(100),
|
|
unique: true,
|
|
allowNull: false
|
|
},
|
|
description: {
|
|
type: Sequelize.TEXT
|
|
},
|
|
icon: {
|
|
type: Sequelize.STRING(255)
|
|
},
|
|
slug: {
|
|
type: Sequelize.STRING(100),
|
|
unique: true,
|
|
allowNull: false
|
|
},
|
|
question_count: {
|
|
type: Sequelize.INTEGER,
|
|
defaultValue: 0
|
|
},
|
|
is_active: {
|
|
type: Sequelize.BOOLEAN,
|
|
defaultValue: true
|
|
},
|
|
guest_accessible: {
|
|
type: Sequelize.BOOLEAN,
|
|
defaultValue: false
|
|
},
|
|
public_question_count: {
|
|
type: Sequelize.INTEGER,
|
|
defaultValue: 0
|
|
},
|
|
registered_question_count: {
|
|
type: Sequelize.INTEGER,
|
|
defaultValue: 0
|
|
},
|
|
created_at: {
|
|
type: Sequelize.DATE,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
},
|
|
updated_at: {
|
|
type: Sequelize.DATE,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
|
|
}
|
|
});
|
|
|
|
// Add indexes
|
|
await queryInterface.addIndex('categories', ['slug']);
|
|
await queryInterface.addIndex('categories', ['is_active']);
|
|
await queryInterface.addIndex('categories', ['guest_accessible']);
|
|
},
|
|
|
|
down: async (queryInterface, Sequelize) => {
|
|
await queryInterface.dropTable('categories');
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Migration 3: Create Questions Table
|
|
|
|
**File**: `migrations/20250101000003-create-questions.js`
|
|
|
|
```javascript
|
|
'use strict';
|
|
|
|
module.exports = {
|
|
up: async (queryInterface, Sequelize) => {
|
|
await queryInterface.createTable('questions', {
|
|
id: {
|
|
type: Sequelize.UUID,
|
|
defaultValue: Sequelize.UUIDV4,
|
|
primaryKey: true
|
|
},
|
|
question: {
|
|
type: Sequelize.TEXT,
|
|
allowNull: false
|
|
},
|
|
type: {
|
|
type: Sequelize.ENUM('multiple', 'trueFalse', 'written'),
|
|
allowNull: false
|
|
},
|
|
category_id: {
|
|
type: Sequelize.UUID,
|
|
allowNull: false,
|
|
references: {
|
|
model: 'categories',
|
|
key: 'id'
|
|
},
|
|
onUpdate: 'CASCADE',
|
|
onDelete: 'RESTRICT'
|
|
},
|
|
difficulty: {
|
|
type: Sequelize.ENUM('easy', 'medium', 'hard'),
|
|
allowNull: false
|
|
},
|
|
options: {
|
|
type: Sequelize.JSON,
|
|
comment: 'Array of answer options for multiple choice questions'
|
|
},
|
|
correct_answer: {
|
|
type: Sequelize.STRING(500)
|
|
},
|
|
explanation: {
|
|
type: Sequelize.TEXT,
|
|
allowNull: false
|
|
},
|
|
keywords: {
|
|
type: Sequelize.JSON,
|
|
comment: 'Array of keywords for search'
|
|
},
|
|
tags: {
|
|
type: Sequelize.JSON,
|
|
comment: 'Array of tags'
|
|
},
|
|
visibility: {
|
|
type: Sequelize.ENUM('public', 'registered', 'premium'),
|
|
defaultValue: 'registered'
|
|
},
|
|
is_guest_accessible: {
|
|
type: Sequelize.BOOLEAN,
|
|
defaultValue: false
|
|
},
|
|
created_by: {
|
|
type: Sequelize.UUID,
|
|
references: {
|
|
model: 'users',
|
|
key: 'id'
|
|
},
|
|
onUpdate: 'CASCADE',
|
|
onDelete: 'SET NULL'
|
|
},
|
|
created_at: {
|
|
type: Sequelize.DATE,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
},
|
|
updated_at: {
|
|
type: Sequelize.DATE,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
|
|
},
|
|
is_active: {
|
|
type: Sequelize.BOOLEAN,
|
|
defaultValue: true
|
|
},
|
|
times_attempted: {
|
|
type: Sequelize.INTEGER,
|
|
defaultValue: 0
|
|
},
|
|
correct_rate: {
|
|
type: Sequelize.DECIMAL(5, 2),
|
|
defaultValue: 0.00
|
|
}
|
|
});
|
|
|
|
// Add indexes
|
|
await queryInterface.addIndex('questions', ['category_id']);
|
|
await queryInterface.addIndex('questions', ['difficulty']);
|
|
await queryInterface.addIndex('questions', ['type']);
|
|
await queryInterface.addIndex('questions', ['visibility']);
|
|
await queryInterface.addIndex('questions', ['is_active']);
|
|
await queryInterface.addIndex('questions', ['is_guest_accessible']);
|
|
|
|
// Add full-text index
|
|
await queryInterface.sequelize.query(
|
|
'CREATE FULLTEXT INDEX idx_questions_fulltext ON questions(question, explanation)'
|
|
);
|
|
},
|
|
|
|
down: async (queryInterface, Sequelize) => {
|
|
await queryInterface.dropTable('questions');
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Migration 4: Create Guest Sessions Table
|
|
|
|
**File**: `migrations/20250101000004-create-guest-sessions.js`
|
|
|
|
```javascript
|
|
'use strict';
|
|
|
|
module.exports = {
|
|
up: async (queryInterface, Sequelize) => {
|
|
await queryInterface.createTable('guest_sessions', {
|
|
id: {
|
|
type: Sequelize.UUID,
|
|
defaultValue: Sequelize.UUIDV4,
|
|
primaryKey: true
|
|
},
|
|
guest_id: {
|
|
type: Sequelize.STRING(100),
|
|
unique: true,
|
|
allowNull: false
|
|
},
|
|
device_id: {
|
|
type: Sequelize.STRING(255),
|
|
allowNull: false
|
|
},
|
|
session_token: {
|
|
type: Sequelize.STRING(500),
|
|
allowNull: false
|
|
},
|
|
quizzes_attempted: {
|
|
type: Sequelize.INTEGER,
|
|
defaultValue: 0
|
|
},
|
|
max_quizzes: {
|
|
type: Sequelize.INTEGER,
|
|
defaultValue: 3
|
|
},
|
|
created_at: {
|
|
type: Sequelize.DATE,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
},
|
|
expires_at: {
|
|
type: Sequelize.DATE,
|
|
allowNull: false
|
|
},
|
|
ip_address: {
|
|
type: Sequelize.STRING(45)
|
|
},
|
|
user_agent: {
|
|
type: Sequelize.TEXT
|
|
}
|
|
});
|
|
|
|
// Add indexes
|
|
await queryInterface.addIndex('guest_sessions', ['guest_id']);
|
|
await queryInterface.addIndex('guest_sessions', ['session_token']);
|
|
await queryInterface.addIndex('guest_sessions', ['expires_at']);
|
|
},
|
|
|
|
down: async (queryInterface, Sequelize) => {
|
|
await queryInterface.dropTable('guest_sessions');
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Migration 5: Create Quiz Sessions Table
|
|
|
|
**File**: `migrations/20250101000005-create-quiz-sessions.js`
|
|
|
|
```javascript
|
|
'use strict';
|
|
|
|
module.exports = {
|
|
up: async (queryInterface, Sequelize) => {
|
|
await queryInterface.createTable('quiz_sessions', {
|
|
id: {
|
|
type: Sequelize.UUID,
|
|
defaultValue: Sequelize.UUIDV4,
|
|
primaryKey: true
|
|
},
|
|
user_id: {
|
|
type: Sequelize.UUID,
|
|
references: {
|
|
model: 'users',
|
|
key: 'id'
|
|
},
|
|
onUpdate: 'CASCADE',
|
|
onDelete: 'CASCADE'
|
|
},
|
|
guest_session_id: {
|
|
type: Sequelize.UUID,
|
|
references: {
|
|
model: 'guest_sessions',
|
|
key: 'id'
|
|
},
|
|
onUpdate: 'CASCADE',
|
|
onDelete: 'CASCADE'
|
|
},
|
|
is_guest_session: {
|
|
type: Sequelize.BOOLEAN,
|
|
defaultValue: false
|
|
},
|
|
category_id: {
|
|
type: Sequelize.UUID,
|
|
references: {
|
|
model: 'categories',
|
|
key: 'id'
|
|
},
|
|
onUpdate: 'CASCADE',
|
|
onDelete: 'SET NULL'
|
|
},
|
|
start_time: {
|
|
type: Sequelize.DATE,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
},
|
|
end_time: {
|
|
type: Sequelize.DATE,
|
|
allowNull: true
|
|
},
|
|
score: {
|
|
type: Sequelize.INTEGER,
|
|
defaultValue: 0
|
|
},
|
|
total_questions: {
|
|
type: Sequelize.INTEGER,
|
|
allowNull: false
|
|
},
|
|
status: {
|
|
type: Sequelize.ENUM('in-progress', 'completed', 'abandoned'),
|
|
defaultValue: 'in-progress'
|
|
},
|
|
completed_at: {
|
|
type: Sequelize.DATE,
|
|
allowNull: true
|
|
},
|
|
created_at: {
|
|
type: Sequelize.DATE,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
},
|
|
updated_at: {
|
|
type: Sequelize.DATE,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
|
|
}
|
|
});
|
|
|
|
// Add indexes
|
|
await queryInterface.addIndex('quiz_sessions', ['user_id']);
|
|
await queryInterface.addIndex('quiz_sessions', ['guest_session_id']);
|
|
await queryInterface.addIndex('quiz_sessions', ['status']);
|
|
await queryInterface.addIndex('quiz_sessions', ['completed_at']);
|
|
},
|
|
|
|
down: async (queryInterface, Sequelize) => {
|
|
await queryInterface.dropTable('quiz_sessions');
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Seeder Example: Demo Categories
|
|
|
|
**File**: `seeders/20250101000001-demo-categories.js`
|
|
|
|
```javascript
|
|
'use strict';
|
|
const { v4: uuidv4 } = require('uuid');
|
|
|
|
module.exports = {
|
|
up: async (queryInterface, Sequelize) => {
|
|
const categories = [
|
|
{
|
|
id: uuidv4(),
|
|
name: 'Angular',
|
|
description: 'Frontend framework by Google',
|
|
icon: 'angular-icon.svg',
|
|
slug: 'angular',
|
|
question_count: 0,
|
|
is_active: true,
|
|
guest_accessible: true,
|
|
public_question_count: 0,
|
|
registered_question_count: 0,
|
|
created_at: new Date(),
|
|
updated_at: new Date()
|
|
},
|
|
{
|
|
id: uuidv4(),
|
|
name: 'Node.js',
|
|
description: 'JavaScript runtime for backend',
|
|
icon: 'nodejs-icon.svg',
|
|
slug: 'nodejs',
|
|
question_count: 0,
|
|
is_active: true,
|
|
guest_accessible: true,
|
|
public_question_count: 0,
|
|
registered_question_count: 0,
|
|
created_at: new Date(),
|
|
updated_at: new Date()
|
|
},
|
|
{
|
|
id: uuidv4(),
|
|
name: 'MySQL',
|
|
description: 'Relational database management system',
|
|
icon: 'mysql-icon.svg',
|
|
slug: 'mysql',
|
|
question_count: 0,
|
|
is_active: true,
|
|
guest_accessible: false,
|
|
public_question_count: 0,
|
|
registered_question_count: 0,
|
|
created_at: new Date(),
|
|
updated_at: new Date()
|
|
},
|
|
{
|
|
id: uuidv4(),
|
|
name: 'Express.js',
|
|
description: 'Web framework for Node.js',
|
|
icon: 'express-icon.svg',
|
|
slug: 'expressjs',
|
|
question_count: 0,
|
|
is_active: true,
|
|
guest_accessible: true,
|
|
public_question_count: 0,
|
|
registered_question_count: 0,
|
|
created_at: new Date(),
|
|
updated_at: new Date()
|
|
},
|
|
{
|
|
id: uuidv4(),
|
|
name: 'JavaScript',
|
|
description: 'Core programming language',
|
|
icon: 'javascript-icon.svg',
|
|
slug: 'javascript',
|
|
question_count: 0,
|
|
is_active: true,
|
|
guest_accessible: true,
|
|
public_question_count: 0,
|
|
registered_question_count: 0,
|
|
created_at: new Date(),
|
|
updated_at: new Date()
|
|
}
|
|
];
|
|
|
|
await queryInterface.bulkInsert('categories', categories);
|
|
},
|
|
|
|
down: async (queryInterface, Sequelize) => {
|
|
await queryInterface.bulkDelete('categories', null, {});
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Seeder Example: Demo Admin User
|
|
|
|
**File**: `seeders/20250101000002-demo-admin.js`
|
|
|
|
```javascript
|
|
'use strict';
|
|
const bcrypt = require('bcrypt');
|
|
const { v4: uuidv4 } = require('uuid');
|
|
|
|
module.exports = {
|
|
up: async (queryInterface, Sequelize) => {
|
|
const hashedPassword = await bcrypt.hash('Admin@123', 10);
|
|
|
|
await queryInterface.bulkInsert('users', [{
|
|
id: uuidv4(),
|
|
username: 'admin',
|
|
email: 'admin@quizapp.com',
|
|
password: hashedPassword,
|
|
role: 'admin',
|
|
profile_image: null,
|
|
created_at: new Date(),
|
|
updated_at: new Date(),
|
|
last_login: null,
|
|
total_quizzes: 0,
|
|
total_questions: 0,
|
|
correct_answers: 0,
|
|
streak: 0
|
|
}]);
|
|
},
|
|
|
|
down: async (queryInterface, Sequelize) => {
|
|
await queryInterface.bulkDelete('users', { email: 'admin@quizapp.com' }, {});
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Running Migrations
|
|
|
|
```bash
|
|
# Create database first
|
|
mysql -u root -p
|
|
CREATE DATABASE interview_quiz_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
EXIT;
|
|
|
|
# Run all migrations
|
|
npx sequelize-cli db:migrate
|
|
|
|
# Check migration status
|
|
npx sequelize-cli db:migrate:status
|
|
|
|
# Undo last migration
|
|
npx sequelize-cli db:migrate:undo
|
|
|
|
# Undo all migrations
|
|
npx sequelize-cli db:migrate:undo:all
|
|
|
|
# Run seeders
|
|
npx sequelize-cli db:seed:all
|
|
|
|
# Undo seeders
|
|
npx sequelize-cli db:seed:undo:all
|
|
```
|
|
|
|
---
|
|
|
|
## .sequelizerc Configuration
|
|
|
|
**File**: `.sequelizerc` (in project root)
|
|
|
|
```javascript
|
|
const path = require('path');
|
|
|
|
module.exports = {
|
|
'config': path.resolve('config', 'database.js'),
|
|
'models-path': path.resolve('models'),
|
|
'seeders-path': path.resolve('seeders'),
|
|
'migrations-path': path.resolve('migrations')
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Database Configuration
|
|
|
|
**File**: `config/database.js`
|
|
|
|
```javascript
|
|
require('dotenv').config();
|
|
|
|
module.exports = {
|
|
development: {
|
|
username: process.env.DB_USER || 'root',
|
|
password: process.env.DB_PASSWORD || '',
|
|
database: process.env.DB_NAME || 'interview_quiz_db',
|
|
host: process.env.DB_HOST || 'localhost',
|
|
port: process.env.DB_PORT || 3306,
|
|
dialect: 'mysql',
|
|
logging: console.log,
|
|
pool: {
|
|
max: 10,
|
|
min: 0,
|
|
acquire: 30000,
|
|
idle: 10000
|
|
}
|
|
},
|
|
test: {
|
|
username: process.env.DB_USER || 'root',
|
|
password: process.env.DB_PASSWORD || '',
|
|
database: 'interview_quiz_test',
|
|
host: process.env.DB_HOST || 'localhost',
|
|
port: process.env.DB_PORT || 3306,
|
|
dialect: 'mysql',
|
|
logging: false
|
|
},
|
|
production: {
|
|
username: process.env.DB_USER,
|
|
password: process.env.DB_PASSWORD,
|
|
database: process.env.DB_NAME,
|
|
host: process.env.DB_HOST,
|
|
port: process.env.DB_PORT || 3306,
|
|
dialect: 'mysql',
|
|
logging: false,
|
|
pool: {
|
|
max: 20,
|
|
min: 5,
|
|
acquire: 30000,
|
|
idle: 10000
|
|
},
|
|
dialectOptions: {
|
|
ssl: {
|
|
require: true,
|
|
rejectUnauthorized: false
|
|
}
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
That's it! Your migration files are ready to use. 🚀
|