ready to test

Testing

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-06-22 10:06:58 +08:00
parent bf48c95467
commit 439bb33f77
189 changed files with 17559 additions and 686 deletions
@@ -0,0 +1,30 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('users', {
user_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
email: { type: Sequelize.STRING(255), allowNull: false, unique: true },
password: { type: Sequelize.TEXT },
is_active: { type: Sequelize.BOOLEAN, defaultValue: true },
is_verified: { type: Sequelize.BOOLEAN, defaultValue: false },
reg_type: { type: Sequelize.ENUM('google', 'system'), defaultValue: 'system' },
acc_type: { type: Sequelize.ENUM('admin', 'staff', 'user'), defaultValue: 'user' },
personal_info: { type: Sequelize.JSONB, defaultValue: null },
must_change_password: { type: Sequelize.BOOLEAN, defaultValue: false, allowNull: false },
password_expires_at: { type: Sequelize.DATE, allowNull: true },
otp_code: { type: Sequelize.STRING(6), allowNull: true },
otp_expires_at: { type: Sequelize.DATE, allowNull: true },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
deletedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
},
async down(queryInterface) {
await queryInterface.dropTable('users');
},
};
@@ -0,0 +1,47 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('assets', {
asset_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
uuid: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, allowNull: false, unique: true },
original_name: { type: Sequelize.STRING(255), allowNull: false },
display_name: { type: Sequelize.STRING(255), allowNull: false },
file_url: { type: Sequelize.STRING(512), allowNull: false },
file_size: { type: Sequelize.BIGINT, allowNull: false },
mime_type: { type: Sequelize.STRING(100), allowNull: false },
extension: { type: Sequelize.STRING(20) },
checksum: { type: Sequelize.STRING(64) },
file_type: { type: Sequelize.ENUM('avatar', 'document', 'video', 'image'), allowNull: false, defaultValue: 'image' },
width: { type: Sequelize.INTEGER },
height: { type: Sequelize.INTEGER },
duration: { type: Sequelize.FLOAT },
resolution: { type: Sequelize.STRING(20) },
frame_rate: { type: Sequelize.FLOAT },
bitrate: { type: Sequelize.BIGINT },
video_codec: { type: Sequelize.STRING(50) },
audio_codec: { type: Sequelize.STRING(50) },
thumbnail_url: { type: Sequelize.STRING(512) },
description: { type: Sequelize.TEXT },
storage_provider: { type: Sequelize.ENUM('local', 's3', 'gcs', 'cloudinary', 'chibisafe', 'other'), defaultValue: 'local' },
storage_bucket: { type: Sequelize.STRING(255) },
storage_key: { type: Sequelize.STRING(512) },
is_public: { type: Sequelize.BOOLEAN, defaultValue: false },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
deletedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
await queryInterface.addIndex('assets', ['uuid']);
await queryInterface.addIndex('assets', ['createdBy']);
await queryInterface.addIndex('assets', ['file_type']);
await queryInterface.addIndex('assets', ['deletedAt']);
},
async down(queryInterface) {
await queryInterface.dropTable('assets');
},
};
@@ -0,0 +1,26 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('user_sessions', {
session_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
user_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'users', key: 'user_id' }, onUpdate: 'CASCADE', onDelete: 'CASCADE' },
login_info: { type: Sequelize.JSONB, allowNull: true },
logout_info: { type: Sequelize.JSONB, allowNull: true },
refresh_token_hash: { type: Sequelize.TEXT, allowNull: true },
is_active: { type: Sequelize.BOOLEAN, defaultValue: true },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
deletedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
await queryInterface.addIndex('user_sessions', ['user_id']);
},
async down(queryInterface) {
await queryInterface.dropTable('user_sessions');
},
};
@@ -0,0 +1,23 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('user_groups', {
group_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
name: { type: Sequelize.STRING(50), allowNull: false },
group_code: { type: Sequelize.STRING(50), allowNull: true, unique: true },
description: { type: Sequelize.TEXT },
is_active: { type: Sequelize.BOOLEAN, defaultValue: true },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
deletedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
},
async down(queryInterface) {
await queryInterface.dropTable('user_groups');
},
};
@@ -0,0 +1,24 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('user_group_members', {
group_id: { type: Sequelize.BIGINT, primaryKey: true, references: { model: 'user_groups', key: 'group_id' }, onDelete: 'CASCADE' },
user_id: { type: Sequelize.BIGINT, primaryKey: true, references: { model: 'users', key: 'user_id' }, onDelete: 'CASCADE' },
joined_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
deletedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
await queryInterface.addIndex('user_group_members', ['user_id']);
await queryInterface.addIndex('user_group_members', ['group_id']);
},
async down(queryInterface) {
await queryInterface.dropTable('user_group_members');
},
};
@@ -0,0 +1,26 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('achievements', {
achievement_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
user_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'users', key: 'user_id' }, onDelete: 'CASCADE' },
type: { type: Sequelize.ENUM('badge', 'milestone'), allowNull: false, defaultValue: 'badge' },
key: { type: Sequelize.STRING(100), allowNull: false },
label: { type: Sequelize.STRING(255), allowNull: false },
description: { type: Sequelize.TEXT, allowNull: true },
granted_by: { type: Sequelize.BIGINT, allowNull: true },
granted_at: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.NOW },
metadata: { type: Sequelize.JSONB, allowNull: true, defaultValue: {} },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.addIndex('achievements', ['user_id']);
await queryInterface.addIndex('achievements', { fields: ['user_id', 'key'], unique: true, name: 'uq_achievement_user_key' });
},
async down(queryInterface) {
await queryInterface.dropTable('achievements');
},
};
@@ -0,0 +1,31 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('courses', {
course_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
uuid: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, allowNull: false, unique: true },
title: { type: Sequelize.STRING(255), allowNull: false },
description: { type: Sequelize.TEXT, allowNull: true },
course_code: { type: Sequelize.STRING(50), allowNull: true, unique: true },
order_index: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
level: { type: Sequelize.ENUM('beginner', 'intermediate', 'advanced'), allowNull: true },
subscription: { type: Sequelize.ENUM('free', 'premium'), allowNull: false, defaultValue: 'free' },
duration_seconds: { type: Sequelize.INTEGER, allowNull: true, defaultValue: 0 },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
deletedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
await queryInterface.addIndex('courses', ['uuid']);
await queryInterface.addIndex('courses', ['subscription']);
await queryInterface.addIndex('courses', ['deletedAt']);
},
async down(queryInterface) {
await queryInterface.dropTable('courses');
},
};
@@ -0,0 +1,35 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('course_products', {
id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
course_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'courses', key: 'course_id' }, onDelete: 'CASCADE' },
product_id: { type: Sequelize.BIGINT, allowNull: false },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.createTable('course_roles', {
id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
course_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'courses', key: 'course_id' }, onDelete: 'CASCADE' },
role_id: { type: Sequelize.BIGINT, allowNull: false },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.createTable('course_product_categories', {
id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
course_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'courses', key: 'course_id' }, onDelete: 'CASCADE' },
category_id: { type: Sequelize.BIGINT, allowNull: false },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
},
async down(queryInterface) {
await queryInterface.dropTable('course_product_categories');
await queryInterface.dropTable('course_roles');
await queryInterface.dropTable('course_products');
},
};
@@ -0,0 +1,21 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('course_objectives', {
objective_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
course_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'courses', key: 'course_id' }, onDelete: 'CASCADE' },
text: { type: Sequelize.TEXT, allowNull: false },
order_index: { type: Sequelize.INTEGER, defaultValue: 0 },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
await queryInterface.addIndex('course_objectives', ['course_id']);
},
async down(queryInterface) {
await queryInterface.dropTable('course_objectives');
},
};
@@ -0,0 +1,21 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('course_prerequisites', {
prereq_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
course_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'courses', key: 'course_id' }, onDelete: 'CASCADE' },
ref_type: { type: Sequelize.ENUM('course', 'unit', 'lesson'), allowNull: false },
ref_id: { type: Sequelize.BIGINT, allowNull: false },
order_index: { type: Sequelize.INTEGER, defaultValue: 0 },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.addIndex('course_prerequisites', ['course_id']);
},
async down(queryInterface) {
await queryInterface.dropTable('course_prerequisites');
},
};
@@ -0,0 +1,26 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('course_assessments', {
assessment_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
uuid: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, unique: true },
course_id: { type: Sequelize.BIGINT, allowNull: false, unique: true, references: { model: 'courses', key: 'course_id' }, onDelete: 'CASCADE' },
title: { type: Sequelize.STRING(255), allowNull: true },
is_required: { type: Sequelize.BOOLEAN, defaultValue: false },
passing_score: { type: Sequelize.INTEGER, defaultValue: 70 },
time_limit_minutes: { type: Sequelize.INTEGER, allowNull: true },
max_questions: { type: Sequelize.INTEGER, allowNull: true },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
deletedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
},
async down(queryInterface) {
await queryInterface.dropTable('course_assessments');
},
};
@@ -0,0 +1,30 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('units', {
unit_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
uuid: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, allowNull: false, unique: true },
course_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'courses', key: 'course_id' }, onDelete: 'CASCADE' },
title: { type: Sequelize.STRING(255), allowNull: false },
description: { type: Sequelize.TEXT, allowNull: true },
order_index: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
duration_seconds: { type: Sequelize.INTEGER, allowNull: true, defaultValue: 0 },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
deletedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
await queryInterface.addIndex('units', ['uuid']);
await queryInterface.addIndex('units', ['course_id']);
await queryInterface.addIndex('units', ['order_index']);
await queryInterface.addIndex('units', ['deletedAt']);
},
async down(queryInterface) {
await queryInterface.dropTable('units');
},
};
@@ -0,0 +1,25 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('unit_quizzes', {
quiz_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
uuid: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, unique: true },
unit_id: { type: Sequelize.BIGINT, allowNull: false, unique: true, references: { model: 'units', key: 'unit_id' }, onDelete: 'CASCADE' },
title: { type: Sequelize.STRING(255), allowNull: true },
is_required: { type: Sequelize.BOOLEAN, defaultValue: false },
passing_score: { type: Sequelize.INTEGER, defaultValue: 70 },
max_questions: { type: Sequelize.INTEGER, allowNull: true },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
deletedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
},
async down(queryInterface) {
await queryInterface.dropTable('unit_quizzes');
},
};
@@ -0,0 +1,30 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('lessons', {
lesson_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
uuid: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, allowNull: false, unique: true },
unit_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'units', key: 'unit_id' }, onDelete: 'CASCADE' },
title: { type: Sequelize.STRING(255), allowNull: false },
description: { type: Sequelize.TEXT, allowNull: true },
duration_seconds: { type: Sequelize.INTEGER, allowNull: true, defaultValue: 0 },
order_index: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
deletedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
await queryInterface.addIndex('lessons', ['uuid']);
await queryInterface.addIndex('lessons', ['unit_id']);
await queryInterface.addIndex('lessons', ['order_index']);
await queryInterface.addIndex('lessons', ['deletedAt']);
},
async down(queryInterface) {
await queryInterface.dropTable('lessons');
},
};
@@ -0,0 +1,19 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('lesson_pages', {
page_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
lesson_id: { type: Sequelize.BIGINT, allowNull: false, unique: true, references: { model: 'lessons', key: 'lesson_id' }, onDelete: 'CASCADE' },
blocks: { type: Sequelize.JSONB, allowNull: false, defaultValue: [] },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
},
async down(queryInterface) {
await queryInterface.dropTable('lesson_pages');
},
};
@@ -0,0 +1,21 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('lesson_objectives', {
objective_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
lesson_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'lessons', key: 'lesson_id' }, onDelete: 'CASCADE' },
text: { type: Sequelize.TEXT, allowNull: false },
order_index: { type: Sequelize.INTEGER, defaultValue: 0 },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
await queryInterface.addIndex('lesson_objectives', ['lesson_id']);
},
async down(queryInterface) {
await queryInterface.dropTable('lesson_objectives');
},
};
@@ -0,0 +1,29 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('quiz_questions', {
question_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
uuid: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, unique: true },
quiz_id: { type: Sequelize.BIGINT, allowNull: true, references: { model: 'unit_quizzes', key: 'quiz_id' }, onDelete: 'CASCADE' },
assessment_id: { type: Sequelize.BIGINT, allowNull: true, references: { model: 'course_assessments', key: 'assessment_id' }, onDelete: 'CASCADE' },
type: { type: Sequelize.ENUM('true_false', 'multiple_choice', 'multi_select'), allowNull: false },
question: { type: Sequelize.TEXT, allowNull: false },
explanation: { type: Sequelize.TEXT, allowNull: true },
order_index: { type: Sequelize.INTEGER, defaultValue: 0 },
points: { type: Sequelize.INTEGER, defaultValue: 1 },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
deletedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
await queryInterface.addIndex('quiz_questions', ['quiz_id']);
await queryInterface.addIndex('quiz_questions', ['assessment_id']);
},
async down(queryInterface) {
await queryInterface.dropTable('quiz_questions');
},
};
@@ -0,0 +1,21 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('quiz_options', {
option_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
question_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'quiz_questions', key: 'question_id' }, onDelete: 'CASCADE' },
text: { type: Sequelize.TEXT, allowNull: false },
is_correct: { type: Sequelize.BOOLEAN, defaultValue: false },
order_index: { type: Sequelize.INTEGER, defaultValue: 0 },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.addIndex('quiz_options', ['question_id']);
},
async down(queryInterface) {
await queryInterface.dropTable('quiz_options');
},
};
@@ -0,0 +1,28 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('quiz_attempts', {
attempt_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
uuid: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, unique: true },
user_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'users', key: 'user_id' }, onDelete: 'CASCADE' },
quiz_id: { type: Sequelize.BIGINT, allowNull: true, references: { model: 'unit_quizzes', key: 'quiz_id' }, onDelete: 'SET NULL' },
assessment_id: { type: Sequelize.BIGINT, allowNull: true, references: { model: 'course_assessments', key: 'assessment_id' }, onDelete: 'SET NULL' },
answers: { type: Sequelize.JSONB, allowNull: false, defaultValue: {} },
total_points: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
earned_points: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
score: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
passed: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.addIndex('quiz_attempts', ['user_id']);
await queryInterface.addIndex('quiz_attempts', ['quiz_id']);
await queryInterface.addIndex('quiz_attempts', ['assessment_id']);
},
async down(queryInterface) {
await queryInterface.dropTable('quiz_attempts');
},
};
@@ -0,0 +1,22 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('tier_plans', {
plan_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
tier: { type: Sequelize.ENUM('premium', 'exclusive'), allowNull: false },
label: { type: Sequelize.STRING(100), allowNull: false },
duration_days: { type: Sequelize.INTEGER, allowNull: false },
price: { type: Sequelize.DECIMAL(10, 2), allowNull: false },
currency: { type: Sequelize.CHAR(3), allowNull: false, defaultValue: 'USD' },
is_active: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
},
async down(queryInterface) {
await queryInterface.dropTable('tier_plans');
},
};
@@ -0,0 +1,19 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('plan_courses', {
id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
plan_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'tier_plans', key: 'plan_id' }, onDelete: 'CASCADE' },
course_id: { type: Sequelize.BIGINT, allowNull: false, unique: true, references: { model: 'courses', key: 'course_id' }, onDelete: 'CASCADE' },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.addIndex('plan_courses', ['plan_id']);
},
async down(queryInterface) {
await queryInterface.dropTable('plan_courses');
},
};
@@ -0,0 +1,27 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('user_tiers', {
tier_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
user_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'users', key: 'user_id' }, onDelete: 'CASCADE' },
tier: { type: Sequelize.ENUM('free', 'premium', 'exclusive'), allowNull: false, defaultValue: 'free' },
status: { type: Sequelize.ENUM('active', 'expired', 'revoked'), allowNull: false, defaultValue: 'active' },
starts_at: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.NOW },
expires_at: { type: Sequelize.DATE, allowNull: true },
granted_by: { type: Sequelize.BIGINT, allowNull: true },
revoked_by: { type: Sequelize.BIGINT, allowNull: true },
revoked_at: { type: Sequelize.DATE, allowNull: true },
notes: { type: Sequelize.TEXT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.addIndex('user_tiers', ['user_id']);
await queryInterface.addIndex('user_tiers', ['status']);
},
async down(queryInterface) {
await queryInterface.dropTable('user_tiers');
},
};
@@ -0,0 +1,34 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('payments', {
payment_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
user_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'users', key: 'user_id' }, onDelete: 'CASCADE' },
plan_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'tier_plans', key: 'plan_id' }, onDelete: 'RESTRICT' },
tier_id: { type: Sequelize.BIGINT, allowNull: true, references: { model: 'user_tiers', key: 'tier_id' }, onDelete: 'SET NULL' },
status: { type: Sequelize.ENUM('pending', 'completed', 'failed', 'cancelled', 'expired', 'refunded'), allowNull: false, defaultValue: 'pending' },
amount: { type: Sequelize.DECIMAL(10, 2), allowNull: false },
currency: { type: Sequelize.STRING(10), allowNull: false, defaultValue: 'USD' },
promo_code: { type: Sequelize.STRING(50), allowNull: true },
discount: { type: Sequelize.DECIMAL(10, 2), allowNull: false, defaultValue: 0.00 },
provider: { type: Sequelize.STRING(50), allowNull: false, defaultValue: 'paypal' },
provider_payload: { type: Sequelize.JSONB, allowNull: true, defaultValue: {} },
paid_at: { type: Sequelize.DATE, allowNull: true },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
deletedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
await queryInterface.addIndex('payments', ['user_id']);
await queryInterface.addIndex('payments', ['status']);
await queryInterface.addIndex('payments', ['plan_id']);
},
async down(queryInterface) {
await queryInterface.dropTable('payments');
},
};
@@ -0,0 +1,40 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('advertisements', {
advertisement_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
uuid: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, allowNull: false, unique: true },
type: { type: Sequelize.ENUM('hero', 'banner', 'popup', 'sidebar'), allowNull: false },
status: { type: Sequelize.ENUM('draft', 'active', 'scheduled', 'expired', 'archived'), allowNull: false, defaultValue: 'draft' },
badge_label: { type: Sequelize.STRING(100) },
headline: { type: Sequelize.STRING(255) },
description: { type: Sequelize.TEXT },
image_url: { type: Sequelize.STRING(512) },
image_asset_id: { type: Sequelize.BIGINT, allowNull: true, references: { model: 'assets', key: 'asset_id' }, onDelete: 'SET NULL' },
ctas: { type: Sequelize.JSONB, allowNull: false, defaultValue: [] },
start_date: { type: Sequelize.DATE },
end_date: { type: Sequelize.DATE },
order: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
is_active: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: true },
size: { type: Sequelize.ENUM('sm', 'md', 'lg'), allowNull: true },
click_count: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
deletedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
await queryInterface.addIndex('advertisements', ['uuid']);
await queryInterface.addIndex('advertisements', ['type']);
await queryInterface.addIndex('advertisements', ['status']);
await queryInterface.addIndex('advertisements', ['is_active']);
await queryInterface.addIndex('advertisements', ['deletedAt']);
},
async down(queryInterface) {
await queryInterface.dropTable('advertisements');
},
};
@@ -0,0 +1,21 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('task_lists', {
task_list_id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true },
name: { type: Sequelize.STRING, allowNull: false },
description: { type: Sequelize.TEXT, allowNull: true },
createdBy: { type: Sequelize.INTEGER, allowNull: true },
updatedBy: { type: Sequelize.INTEGER, allowNull: true },
deletedBy: { type: Sequelize.INTEGER, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
},
async down(queryInterface) {
await queryInterface.dropTable('task_lists');
},
};
@@ -0,0 +1,21 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('task_list_groups', {
id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true },
task_list_id: { type: Sequelize.UUID, allowNull: false, references: { model: 'task_lists', key: 'task_list_id' }, onDelete: 'CASCADE' },
group_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'user_groups', key: 'group_id' }, onDelete: 'CASCADE' },
assignedAt: { type: Sequelize.DATE, defaultValue: Sequelize.NOW, allowNull: false },
assignedBy: { type: Sequelize.BIGINT, allowNull: true },
});
await queryInterface.addIndex('task_list_groups', { fields: ['task_list_id', 'group_id'], unique: true, name: 'uq_task_list_group' });
await queryInterface.addIndex('task_list_groups', { fields: ['task_list_id'], name: 'idx_tlg_task_list_id' });
await queryInterface.addIndex('task_list_groups', { fields: ['group_id'], name: 'idx_tlg_group_id' });
},
async down(queryInterface) {
await queryInterface.dropTable('task_list_groups');
},
};
@@ -0,0 +1,27 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('tasks', {
task_id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true },
task_list_id: { type: Sequelize.UUID, allowNull: false, references: { model: 'task_lists', key: 'task_list_id' }, onDelete: 'CASCADE' },
name: { type: Sequelize.STRING, allowNull: false },
description: { type: Sequelize.TEXT, allowNull: true },
deadline: { type: Sequelize.DATE, allowNull: true },
status: { type: Sequelize.ENUM('pending', 'in_progress', 'completed', 'overdue'), defaultValue: 'pending', allowNull: false },
createdBy: { type: Sequelize.INTEGER, allowNull: true },
updatedBy: { type: Sequelize.INTEGER, allowNull: true },
deletedBy: { type: Sequelize.INTEGER, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
await queryInterface.addIndex('tasks', ['task_list_id']);
await queryInterface.addIndex('tasks', ['status']);
},
async down(queryInterface) {
await queryInterface.dropTable('tasks');
},
};
@@ -0,0 +1,30 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('task_requirements', {
requirement_id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true },
task_id: { type: Sequelize.UUID, allowNull: false, references: { model: 'tasks', key: 'task_id' }, onDelete: 'CASCADE' },
type: { type: Sequelize.ENUM('visit_link', 'upload_file', 'read_course', 'read_unit', 'read_lesson'), allowNull: false },
link_url: { type: Sequelize.STRING, allowNull: true },
link_label: { type: Sequelize.STRING, allowNull: true },
allowed_file_types: { type: Sequelize.JSONB, allowNull: true },
max_file_count: { type: Sequelize.INTEGER, allowNull: true, defaultValue: 1 },
reference_id: { type: Sequelize.UUID, allowNull: true },
reference_label: { type: Sequelize.STRING, allowNull: true },
order: { type: Sequelize.INTEGER, defaultValue: 0 },
createdBy: { type: Sequelize.INTEGER, allowNull: true },
updatedBy: { type: Sequelize.INTEGER, allowNull: true },
deletedBy: { type: Sequelize.INTEGER, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
await queryInterface.addIndex('task_requirements', ['task_id']);
},
async down(queryInterface) {
await queryInterface.dropTable('task_requirements');
},
};
@@ -0,0 +1,27 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('task_completions', {
completion_id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true },
task_id: { type: Sequelize.UUID, allowNull: false, references: { model: 'tasks', key: 'task_id' }, onDelete: 'CASCADE' },
user_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'users', key: 'user_id' }, onDelete: 'CASCADE' },
note: { type: Sequelize.TEXT, allowNull: true },
submitted_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW, allowNull: false },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
deletedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
await queryInterface.addIndex('task_completions', { fields: ['task_id'], name: 'idx_tc_task_id' });
await queryInterface.addIndex('task_completions', { fields: ['user_id'], name: 'idx_tc_user_id' });
await queryInterface.addIndex('task_completions', { fields: ['task_id', 'user_id'], name: 'idx_tc_task_user' });
},
async down(queryInterface) {
await queryInterface.dropTable('task_completions');
},
};
@@ -0,0 +1,27 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('task_completion_files', {
file_id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true },
completion_id: { type: Sequelize.UUID, allowNull: false, references: { model: 'task_completions', key: 'completion_id' }, onDelete: 'CASCADE' },
file_url: { type: Sequelize.STRING, allowNull: false },
file_name: { type: Sequelize.STRING, allowNull: false },
file_size: { type: Sequelize.INTEGER, allowNull: true },
mime_type: { type: Sequelize.STRING, allowNull: true },
storage_key: { type: Sequelize.STRING(500), allowNull: true },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
deletedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
await queryInterface.addIndex('task_completion_files', { fields: ['completion_id'], name: 'idx_tcf_completion_id' });
},
async down(queryInterface) {
await queryInterface.dropTable('task_completion_files');
},
};
@@ -0,0 +1,26 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('task_link_visits', {
visit_id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true },
task_id: { type: Sequelize.UUID, allowNull: false, references: { model: 'tasks', key: 'task_id' }, onDelete: 'CASCADE' },
requirement_id: { type: Sequelize.UUID, allowNull: false, references: { model: 'task_requirements', key: 'requirement_id' }, onDelete: 'CASCADE' },
user_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'users', key: 'user_id' }, onDelete: 'CASCADE' },
visited_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW, allowNull: false },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.addIndex('task_link_visits', { fields: ['requirement_id', 'user_id'], unique: true, name: 'uq_tlv_requirement_user' });
await queryInterface.addIndex('task_link_visits', { fields: ['task_id'], name: 'idx_tlv_task_id' });
await queryInterface.addIndex('task_link_visits', { fields: ['user_id'], name: 'idx_tlv_user_id' });
await queryInterface.addIndex('task_link_visits', { fields: ['requirement_id'], name: 'idx_tlv_requirement_id' });
},
async down(queryInterface) {
await queryInterface.dropTable('task_link_visits');
},
};
@@ -0,0 +1,30 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('task_progress', {
progress_id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true },
task_id: { type: Sequelize.UUID, allowNull: false, references: { model: 'tasks', key: 'task_id' }, onDelete: 'CASCADE' },
requirement_id: { type: Sequelize.UUID, allowNull: false, references: { model: 'task_requirements', key: 'requirement_id' }, onDelete: 'CASCADE' },
user_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'users', key: 'user_id' }, onDelete: 'CASCADE' },
reference_id: { type: Sequelize.UUID, allowNull: false },
type: { type: Sequelize.ENUM('read_course', 'read_unit', 'read_lesson'), allowNull: false },
completed: { type: Sequelize.BOOLEAN, defaultValue: false, allowNull: false },
completed_at: { type: Sequelize.DATE, allowNull: true },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.addIndex('task_progress', { fields: ['requirement_id', 'user_id', 'reference_id'], unique: true, name: 'uq_tp_requirement_user_reference' });
await queryInterface.addIndex('task_progress', { fields: ['task_id'], name: 'idx_tp_task_id' });
await queryInterface.addIndex('task_progress', { fields: ['user_id'], name: 'idx_tp_user_id' });
await queryInterface.addIndex('task_progress', { fields: ['requirement_id'], name: 'idx_tp_requirement_id' });
await queryInterface.addIndex('task_progress', { fields: ['type'], name: 'idx_tp_type' });
},
async down(queryInterface) {
await queryInterface.dropTable('task_progress');
},
};
@@ -0,0 +1,25 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('admin_notifications', {
notification_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
type: { type: Sequelize.STRING(64), allowNull: false },
title: { type: Sequelize.STRING(255), allowNull: false },
message: { type: Sequelize.TEXT, allowNull: false },
data: { type: Sequelize.JSONB, allowNull: true, defaultValue: null },
seen: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false },
seen_at: { type: Sequelize.DATE, allowNull: true, defaultValue: null },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.addIndex('admin_notifications', { fields: ['seen'], name: 'idx_an_seen' });
await queryInterface.addIndex('admin_notifications', { fields: ['type'], name: 'idx_an_type' });
await queryInterface.addIndex('admin_notifications', { fields: ['createdAt'], name: 'idx_an_created_at' });
},
async down(queryInterface) {
await queryInterface.dropTable('admin_notifications');
},
};
@@ -0,0 +1,27 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('user_notifications', {
notification_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
user_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'users', key: 'user_id' }, onDelete: 'CASCADE' },
type: { type: Sequelize.STRING(64), allowNull: false },
title: { type: Sequelize.STRING(255), allowNull: false },
message: { type: Sequelize.TEXT, allowNull: false },
data: { type: Sequelize.JSONB, allowNull: true, defaultValue: null },
seen: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false },
seen_at: { type: Sequelize.DATE, allowNull: true, defaultValue: null },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.addIndex('user_notifications', { fields: ['user_id'], name: 'idx_un_user_id' });
await queryInterface.addIndex('user_notifications', { fields: ['user_id', 'seen'], name: 'idx_un_seen' });
await queryInterface.addIndex('user_notifications', { fields: ['type'], name: 'idx_un_type' });
await queryInterface.addIndex('user_notifications', { fields: ['createdAt'], name: 'idx_un_created_at' });
},
async down(queryInterface) {
await queryInterface.dropTable('user_notifications');
},
};
@@ -0,0 +1,20 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('categories', {
id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
name: { type: Sequelize.STRING(100), allowNull: false, unique: true },
slug: { type: Sequelize.STRING(120), allowNull: false, unique: true },
description: { type: Sequelize.TEXT, allowNull: true },
is_active: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
},
async down(queryInterface) {
await queryInterface.dropTable('categories');
},
};
@@ -0,0 +1,25 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('products', {
id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
course_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'courses', key: 'course_id' }, onDelete: 'CASCADE' },
name: { type: Sequelize.STRING(200), allowNull: false },
description: { type: Sequelize.TEXT, allowNull: true },
price: { type: Sequelize.DECIMAL(10, 2), allowNull: false },
currency: { type: Sequelize.STRING(10), allowNull: false, defaultValue: 'USD' },
access_days: { type: Sequelize.INTEGER, allowNull: true }, // null = lifetime access
is_active: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
await queryInterface.addIndex('products', ['course_id']);
},
async down(queryInterface) {
await queryInterface.dropTable('products');
},
};
@@ -0,0 +1,29 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('course_purchases', {
id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
user_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'users', key: 'user_id' }, onDelete: 'CASCADE' },
product_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'products', key: 'id' }, onDelete: 'RESTRICT' },
amount: { type: Sequelize.DECIMAL(10, 2), allowNull: false },
currency: { type: Sequelize.STRING(10), allowNull: false, defaultValue: 'USD' },
status: { type: Sequelize.ENUM('pending', 'completed', 'failed', 'cancelled', 'refunded'), allowNull: false, defaultValue: 'pending' },
provider: { type: Sequelize.STRING(50), allowNull: false, defaultValue: 'paypal' },
provider_payload: { type: Sequelize.JSONB, allowNull: true, defaultValue: {} },
expires_at: { type: Sequelize.DATE, allowNull: true }, // null = lifetime access
paid_at: { type: Sequelize.DATE, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
deletedAt: { type: Sequelize.DATE, allowNull: true },
});
await queryInterface.addIndex('course_purchases', ['user_id']);
await queryInterface.addIndex('course_purchases', ['product_id']);
await queryInterface.addIndex('course_purchases', ['status']);
},
async down(queryInterface) {
await queryInterface.dropTable('course_purchases');
},
};
@@ -0,0 +1,28 @@
'use strict';
// Wires up the bare product_id and category_id columns that were left
// as plain integers in migration 000008.
module.exports = {
async up(queryInterface) {
await queryInterface.addConstraint('course_products', {
fields: ['product_id'],
type: 'foreign key',
name: 'fk_course_products_product_id',
references: { table: 'products', field: 'id' },
onDelete: 'CASCADE',
});
await queryInterface.addConstraint('course_product_categories', {
fields: ['category_id'],
type: 'foreign key',
name: 'fk_course_product_categories_category_id',
references: { table: 'categories', field: 'id' },
onDelete: 'CASCADE',
});
},
async down(queryInterface) {
await queryInterface.removeConstraint('course_products', 'fk_course_products_product_id');
await queryInterface.removeConstraint('course_product_categories', 'fk_course_product_categories_category_id');
},
};
@@ -0,0 +1,31 @@
'use strict';
module.exports = {
async up(queryInterface) {
await queryInterface.dropTable('file_uploads', { cascade: true });
await queryInterface.dropTable('course_roles', { cascade: true });
await queryInterface.dropTable('course_products', { cascade: true });
},
async down(queryInterface, Sequelize) {
await queryInterface.createTable('file_uploads', {
id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.createTable('course_roles', {
id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
course_id: { type: Sequelize.BIGINT, allowNull: false },
role_id: { type: Sequelize.BIGINT, allowNull: false },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.createTable('course_products', {
id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
course_id: { type: Sequelize.BIGINT, allowNull: false },
product_id: { type: Sequelize.BIGINT, allowNull: false },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
},
};
@@ -0,0 +1,35 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('course_reading_progress', {
progress_id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true },
user_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'users', key: 'user_id' }, onDelete: 'CASCADE' },
course_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'courses', key: 'course_id' }, onDelete: 'CASCADE' },
reference_id: { type: Sequelize.UUID, allowNull: false },
type: { type: Sequelize.ENUM('course', 'unit', 'lesson'), allowNull: false },
status: { type: Sequelize.ENUM('in_progress', 'completed'), allowNull: false, defaultValue: 'in_progress' },
completed_at: { type: Sequelize.DATE, allowNull: true },
last_accessed_at: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') },
createdBy: { type: Sequelize.BIGINT, allowNull: true },
updatedBy: { type: Sequelize.BIGINT, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.addIndex('course_reading_progress', {
fields: ['user_id', 'type', 'reference_id'],
unique: true,
name: 'uq_crp_user_type_reference',
});
await queryInterface.addIndex('course_reading_progress', { fields: ['user_id'], name: 'idx_crp_user_id' });
await queryInterface.addIndex('course_reading_progress', { fields: ['course_id'], name: 'idx_crp_course_id' });
await queryInterface.addIndex('course_reading_progress', { fields: ['type'], name: 'idx_crp_type' });
await queryInterface.addIndex('course_reading_progress', { fields: ['status'], name: 'idx_crp_status' });
},
async down(queryInterface) {
await queryInterface.dropTable('course_reading_progress');
},
};
@@ -0,0 +1,35 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn('quiz_attempts', 'attempt_number', {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 1,
comment: '1-based counter per user + quiz (or user + assessment). Derived at insert time from prior attempt count.',
});
await queryInterface.addColumn('quiz_attempts', 'passing_score', {
type: Sequelize.INTEGER,
allowNull: true,
comment: 'Snapshot of the required passing score at the time of submission.',
});
await queryInterface.addColumn('quiz_attempts', 'course_id', {
type: Sequelize.BIGINT,
allowNull: true,
references: { model: 'courses', key: 'course_id' },
onDelete: 'SET NULL',
comment: 'Denormalized course reference for fast per-course attempt queries.',
});
await queryInterface.addIndex('quiz_attempts', { fields: ['course_id'], name: 'idx_qa_course_id' });
},
async down(queryInterface) {
await queryInterface.removeIndex('quiz_attempts', 'idx_qa_course_id');
await queryInterface.removeColumn('quiz_attempts', 'course_id');
await queryInterface.removeColumn('quiz_attempts', 'passing_score');
await queryInterface.removeColumn('quiz_attempts', 'attempt_number');
},
};
@@ -0,0 +1,34 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('pages', {
id: {
type: Sequelize.BIGINT,
autoIncrement: true,
primaryKey: true,
},
type: {
type: Sequelize.ENUM('landing_page', 'lesson_page', 'course_page'),
allowNull: false,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
});
await queryInterface.addIndex('pages', { fields: ['type'], name: 'idx_pages_type' });
},
async down(queryInterface) {
await queryInterface.dropTable('pages');
await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_pages_type";');
},
};
@@ -0,0 +1,69 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('landing_pages', {
id: {
type: Sequelize.BIGINT,
autoIncrement: true,
primaryKey: true,
},
page_id: {
type: Sequelize.BIGINT,
allowNull: false,
unique: true,
references: { model: 'pages', key: 'id' },
onDelete: 'CASCADE',
},
title: {
type: Sequelize.STRING(255),
allowNull: false,
},
slug: {
type: Sequelize.STRING(255),
allowNull: false,
unique: true,
},
meta_title: {
type: Sequelize.STRING(255),
allowNull: true,
},
meta_description: {
type: Sequelize.TEXT,
allowNull: true,
},
status: {
type: Sequelize.ENUM('draft', 'published', 'archived'),
allowNull: false,
defaultValue: 'draft',
},
published_at: {
type: Sequelize.DATE,
allowNull: true,
},
created_by: {
type: Sequelize.BIGINT,
allowNull: true,
references: { model: 'users', key: 'user_id' },
onDelete: 'SET NULL',
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
});
await queryInterface.addIndex('landing_pages', { fields: ['slug'], name: 'idx_landing_pages_slug' });
await queryInterface.addIndex('landing_pages', { fields: ['status'], name: 'idx_landing_pages_status' });
},
async down(queryInterface) {
await queryInterface.dropTable('landing_pages');
},
};
@@ -0,0 +1,59 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('page_sections', {
id: {
type: Sequelize.BIGINT,
autoIncrement: true,
primaryKey: true,
},
page_id: {
type: Sequelize.BIGINT,
allowNull: false,
references: { model: 'pages', key: 'id' },
onDelete: 'CASCADE',
},
type: {
type: Sequelize.ENUM('hero', 'features', 'cta', 'testimonials', 'faq', 'pricing', 'gallery', 'custom'),
allowNull: false,
},
label: {
type: Sequelize.STRING(255),
allowNull: true,
},
position: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
},
settings: {
type: Sequelize.JSON,
allowNull: true,
comment: 'Background, padding, layout, and other section-level style overrides.',
},
is_visible: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: true,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
});
await queryInterface.addIndex('page_sections', { fields: ['page_id'], name: 'idx_page_sections_page_id' });
await queryInterface.addIndex('page_sections', { fields: ['page_id', 'position'], name: 'idx_page_sections_order' });
},
async down(queryInterface) {
await queryInterface.dropTable('page_sections');
},
};
@@ -0,0 +1,55 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('page_blocks', {
id: {
type: Sequelize.BIGINT,
autoIncrement: true,
primaryKey: true,
},
page_section_id: {
type: Sequelize.BIGINT,
allowNull: false,
references: { model: 'page_sections', key: 'id' },
onDelete: 'CASCADE',
},
type: {
type: Sequelize.ENUM('text', 'image', 'button', 'video', 'form', 'spacer'),
allowNull: false,
},
content: {
type: Sequelize.JSON,
allowNull: true,
comment: 'Block payload — varies by type (e.g. src + alt for image, href + label for button).',
},
position: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
},
settings: {
type: Sequelize.JSON,
allowNull: true,
comment: 'Block-level style overrides: alignment, color, font size, etc.',
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
});
await queryInterface.addIndex('page_blocks', { fields: ['page_section_id'], name: 'idx_page_blocks_section_id' });
await queryInterface.addIndex('page_blocks', { fields: ['page_section_id', 'position'], name: 'idx_page_blocks_order' });
},
async down(queryInterface) {
await queryInterface.dropTable('page_blocks');
},
};
@@ -0,0 +1,35 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('page_templates', {
id: {
type: Sequelize.BIGINT,
autoIncrement: true,
primaryKey: true,
},
name: {
type: Sequelize.STRING(255),
allowNull: false,
},
thumbnail_url: {
type: Sequelize.TEXT,
allowNull: true,
},
structure: {
type: Sequelize.JSON,
allowNull: true,
comment: 'Full snapshot of sections and blocks for this template.',
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
});
},
async down(queryInterface) {
await queryInterface.dropTable('page_templates');
},
};
@@ -0,0 +1,67 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('announcements', {
id: {
type: Sequelize.BIGINT,
autoIncrement: true,
primaryKey: true,
},
title: {
type: Sequelize.STRING(255),
allowNull: false,
},
body: {
type: Sequelize.TEXT,
allowNull: false,
},
scope: {
type: Sequelize.ENUM('global', 'course', 'role'),
allowNull: false,
defaultValue: 'global',
},
priority: {
type: Sequelize.ENUM('low', 'normal', 'urgent'),
allowNull: false,
defaultValue: 'normal',
},
published_by: {
type: Sequelize.BIGINT,
allowNull: true,
references: { model: 'users', key: 'user_id' },
onDelete: 'SET NULL',
},
starts_at: {
type: Sequelize.DATE,
allowNull: true,
},
ends_at: {
type: Sequelize.DATE,
allowNull: true,
},
is_active: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: true,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
});
await queryInterface.addIndex('announcements', { fields: ['scope'], name: 'idx_announcements_scope' });
await queryInterface.addIndex('announcements', { fields: ['is_active'], name: 'idx_announcements_is_active' });
},
async down(queryInterface) {
await queryInterface.dropTable('announcements');
},
};
@@ -0,0 +1,100 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('announcement_courses', {
id: {
type: Sequelize.BIGINT,
autoIncrement: true,
primaryKey: true,
},
announcement_id: {
type: Sequelize.BIGINT,
allowNull: false,
references: { model: 'announcements', key: 'id' },
onDelete: 'CASCADE',
},
course_id: {
type: Sequelize.BIGINT,
allowNull: false,
references: { model: 'courses', key: 'course_id' },
onDelete: 'CASCADE',
},
});
await queryInterface.addConstraint('announcement_courses', {
fields: ['announcement_id', 'course_id'],
type: 'unique',
name: 'uq_announcement_courses',
});
await queryInterface.addIndex('announcement_courses', { fields: ['announcement_id'], name: 'idx_announcement_courses_ann_id' });
await queryInterface.addIndex('announcement_courses', { fields: ['course_id'], name: 'idx_announcement_courses_course_id' });
await queryInterface.createTable('announcement_roles', {
id: {
type: Sequelize.BIGINT,
autoIncrement: true,
primaryKey: true,
},
announcement_id: {
type: Sequelize.BIGINT,
allowNull: false,
references: { model: 'announcements', key: 'id' },
onDelete: 'CASCADE',
},
role: {
type: Sequelize.STRING(50),
allowNull: false,
},
});
await queryInterface.addConstraint('announcement_roles', {
fields: ['announcement_id', 'role'],
type: 'unique',
name: 'uq_announcement_roles',
});
await queryInterface.addIndex('announcement_roles', { fields: ['announcement_id'], name: 'idx_announcement_roles_ann_id' });
await queryInterface.createTable('announcement_reads', {
id: {
type: Sequelize.BIGINT,
autoIncrement: true,
primaryKey: true,
},
announcement_id: {
type: Sequelize.BIGINT,
allowNull: false,
references: { model: 'announcements', key: 'id' },
onDelete: 'CASCADE',
},
user_id: {
type: Sequelize.BIGINT,
allowNull: false,
references: { model: 'users', key: 'user_id' },
onDelete: 'CASCADE',
},
read_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
});
await queryInterface.addConstraint('announcement_reads', {
fields: ['announcement_id', 'user_id'],
type: 'unique',
name: 'uq_announcement_reads',
});
await queryInterface.addIndex('announcement_reads', { fields: ['announcement_id'], name: 'idx_announcement_reads_ann_id' });
await queryInterface.addIndex('announcement_reads', { fields: ['user_id'], name: 'idx_announcement_reads_user_id' });
},
async down(queryInterface) {
await queryInterface.dropTable('announcement_reads');
await queryInterface.dropTable('announcement_roles');
await queryInterface.dropTable('announcement_courses');
},
};
@@ -0,0 +1,28 @@
'use strict';
// NOTE: This table was created manually before migration support was added.
// This migration reflects the final column set including entity_type and entity_id
// which were added via ALTER TABLE on 2026-06-21.
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('user_activity', {
activity_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
user_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'users', key: 'user_id' }, onDelete: 'CASCADE' },
session_id: { type: Sequelize.BIGINT, allowNull: true },
action: { type: Sequelize.STRING(100), allowNull: false },
entity_type: { type: Sequelize.STRING(50), allowNull: true },
entity_id: { type: Sequelize.BIGINT, allowNull: true },
details: { type: Sequelize.JSONB, allowNull: true },
created_at: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') },
});
await queryInterface.addIndex('user_activity', { fields: ['user_id'], name: 'idx_ua_user_id' });
await queryInterface.addIndex('user_activity', { fields: ['action'], name: 'idx_ua_action' });
await queryInterface.addIndex('user_activity', { fields: ['created_at'], name: 'idx_ua_created_at' });
},
async down(queryInterface) {
await queryInterface.dropTable('user_activity');
},
};
@@ -0,0 +1,33 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('page_user_groups', {
page_id: {
type: Sequelize.BIGINT,
allowNull: false,
primaryKey: true,
references: { model: 'pages', key: 'id' },
onDelete: 'CASCADE',
},
group_id: {
type: Sequelize.BIGINT,
allowNull: false,
primaryKey: true,
references: { model: 'user_groups', key: 'group_id' },
onDelete: 'CASCADE',
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
});
await queryInterface.addIndex('page_user_groups', { fields: ['group_id'], name: 'idx_page_user_groups_group_id' });
},
async down(queryInterface) {
await queryInterface.dropTable('page_user_groups');
},
};
@@ -0,0 +1,14 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('assets', 'thumbnail_storage_key', {
type: Sequelize.STRING(512),
allowNull: true,
after: 'thumbnail_url',
});
},
down: async (queryInterface) => {
await queryInterface.removeColumn('assets', 'thumbnail_storage_key');
},
};