pushy

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-06-28 11:29:07 +08:00
parent 463a8d3978
commit 89acdfc239
67 changed files with 2736 additions and 306 deletions
@@ -0,0 +1,34 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('unit_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' },
unit_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'units', key: 'unit_id' }, onDelete: 'CASCADE' },
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('unit_reading_progress', {
fields: ['user_id', 'unit_id'],
unique: true,
name: 'uq_urp_user_unit',
});
await queryInterface.addIndex('unit_reading_progress', { fields: ['user_id'], name: 'idx_urp_user_id' });
await queryInterface.addIndex('unit_reading_progress', { fields: ['course_id'], name: 'idx_urp_course_id' });
await queryInterface.addIndex('unit_reading_progress', { fields: ['unit_id'], name: 'idx_urp_unit_id' });
await queryInterface.addIndex('unit_reading_progress', { fields: ['status'], name: 'idx_urp_status' });
},
async down(queryInterface) {
await queryInterface.dropTable('unit_reading_progress');
},
};
@@ -0,0 +1,36 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('lesson_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' },
unit_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'units', key: 'unit_id' }, onDelete: 'CASCADE' },
lesson_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'lessons', key: 'lesson_id' }, onDelete: 'CASCADE' },
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('lesson_reading_progress', {
fields: ['user_id', 'lesson_id'],
unique: true,
name: 'uq_lrp_user_lesson',
});
await queryInterface.addIndex('lesson_reading_progress', { fields: ['user_id'], name: 'idx_lrp_user_id' });
await queryInterface.addIndex('lesson_reading_progress', { fields: ['course_id'], name: 'idx_lrp_course_id' });
await queryInterface.addIndex('lesson_reading_progress', { fields: ['unit_id'], name: 'idx_lrp_unit_id' });
await queryInterface.addIndex('lesson_reading_progress', { fields: ['lesson_id'], name: 'idx_lrp_lesson_id' });
await queryInterface.addIndex('lesson_reading_progress', { fields: ['status'], name: 'idx_lrp_status' });
},
async down(queryInterface) {
await queryInterface.dropTable('lesson_reading_progress');
},
};
@@ -0,0 +1,26 @@
'use strict';
// CockroachDB stores ENUM columns as VARCHAR + check constraint.
// The original check_subscription constraint only allows 'free' | 'premium'.
// This migration drops and recreates it to include 'exclusive'.
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(
`ALTER TABLE courses DROP CONSTRAINT IF EXISTS check_subscription`
);
await queryInterface.sequelize.query(
`ALTER TABLE courses ADD CONSTRAINT check_subscription
CHECK (subscription IN ('free', 'premium', 'exclusive'))`
);
},
async down(queryInterface) {
await queryInterface.sequelize.query(
`ALTER TABLE courses DROP CONSTRAINT IF EXISTS check_subscription`
);
await queryInterface.sequelize.query(
`ALTER TABLE courses ADD CONSTRAINT check_subscription
CHECK (subscription IN ('free', 'premium'))`
);
},
};
@@ -0,0 +1,17 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn('user_tiers', 'plan_id', {
type: Sequelize.BIGINT,
allowNull: true,
references: { model: 'tier_plans', key: 'plan_id' },
onUpdate: 'CASCADE',
onDelete: 'SET NULL',
});
},
async down(queryInterface) {
await queryInterface.removeColumn('user_tiers', 'plan_id');
},
};
@@ -0,0 +1,25 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('plan_policies', {
policy_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
plan_id: { type: Sequelize.BIGINT, allowNull: false, unique: true,
references: { model: 'tier_plans', key: 'plan_id' },
onUpdate: 'CASCADE', onDelete: 'CASCADE' },
badge_asset_id: { type: Sequelize.BIGINT, allowNull: true,
references: { model: 'assets', key: 'asset_id' },
onUpdate: 'CASCADE', onDelete: 'SET NULL' },
badge_label: { type: Sequelize.STRING(100), allowNull: false, defaultValue: '' },
badge_description: { type: Sequelize.TEXT, allowNull: true },
badge_information: { type: Sequelize.TEXT, allowNull: true },
access_rules: { type: Sequelize.JSONB, allowNull: false, defaultValue: [] },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
},
async down(queryInterface) {
await queryInterface.dropTable('plan_policies');
},
};
@@ -0,0 +1,24 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('system_badges', {
badge_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
key: { type: Sequelize.STRING(50), allowNull: false, unique: true },
asset_id: { type: Sequelize.BIGINT, allowNull: true,
references: { model: 'assets', key: 'asset_id' },
onUpdate: 'CASCADE', onDelete: 'SET NULL' },
label: { type: Sequelize.STRING(100), allowNull: false },
description: { type: Sequelize.TEXT, allowNull: true },
information: { type: Sequelize.TEXT, allowNull: true },
active_from: { type: Sequelize.DATEONLY, allowNull: true },
active_until: { type: Sequelize.DATEONLY, allowNull: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
},
async down(queryInterface) {
await queryInterface.dropTable('system_badges');
},
};
@@ -0,0 +1,36 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('tier_categories', {
tier_category_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
slug: { type: Sequelize.STRING(50), allowNull: false, unique: true },
name: { type: Sequelize.STRING(100), allowNull: false },
description: { type: Sequelize.TEXT, allowNull: true },
rank: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
badge_asset_id: {
type: Sequelize.BIGINT, allowNull: true,
references: { model: 'assets', key: 'asset_id' },
onUpdate: 'CASCADE', onDelete: 'SET NULL',
},
badge_label: { type: Sequelize.STRING(100), allowNull: true },
is_default: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false },
is_active: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: true },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
// Seed the three baseline tier categories
await queryInterface.sequelize.query(`
INSERT INTO tier_categories (slug, name, rank, is_default, is_active, "createdAt", "updatedAt")
VALUES
('free', 'Free', 0, true, true, NOW(), NOW()),
('premium', 'Premium', 1, false, true, NOW(), NOW()),
('exclusive', 'Exclusive', 2, false, true, NOW(), NOW())
`);
},
async down(queryInterface) {
await queryInterface.dropTable('tier_categories');
},
};
@@ -0,0 +1,39 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
// Add the FK column
await queryInterface.addColumn('tier_plans', 'tier_category_id', {
type: Sequelize.BIGINT,
allowNull: true,
references: { model: 'tier_categories', key: 'tier_category_id' },
onUpdate: 'CASCADE',
onDelete: 'SET NULL',
});
// Populate from the existing tier slug → tier_categories row
await queryInterface.sequelize.query(`
UPDATE tier_plans tp
SET tier_category_id = tc.tier_category_id
FROM tier_categories tc
WHERE tc.slug = tp.tier
`);
// CockroachDB stores Sequelize ENUMs as VARCHAR + CHECK constraint.
// Drop the constraint so `tier` becomes a free-form slug that can hold
// any value matching a tier_categories.slug (including admin-created ones).
await queryInterface.sequelize.query(
`ALTER TABLE tier_plans DROP CONSTRAINT IF EXISTS check_tier`
);
await queryInterface.sequelize.query(
`ALTER TABLE tier_plans DROP CONSTRAINT IF EXISTS "tier_plans_tier_check"`
);
},
async down(queryInterface) {
await queryInterface.removeColumn('tier_plans', 'tier_category_id');
await queryInterface.sequelize.query(
`ALTER TABLE tier_plans ADD CONSTRAINT check_tier CHECK (tier IN ('premium', 'exclusive'))`
);
},
};
@@ -0,0 +1,20 @@
'use strict';
// Badge config moves to tier_categories — plan_policies now only holds access_rules.
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`SET sql_safe_updates = false`);
await queryInterface.sequelize.query(`ALTER TABLE plan_policies DROP COLUMN IF EXISTS badge_asset_id`);
await queryInterface.sequelize.query(`ALTER TABLE plan_policies DROP COLUMN IF EXISTS badge_label`);
await queryInterface.sequelize.query(`ALTER TABLE plan_policies DROP COLUMN IF EXISTS badge_description`);
await queryInterface.sequelize.query(`ALTER TABLE plan_policies DROP COLUMN IF EXISTS badge_information`);
},
async down(queryInterface, Sequelize) {
await queryInterface.addColumn('plan_policies', 'badge_asset_id', { type: Sequelize.BIGINT, allowNull: true });
await queryInterface.addColumn('plan_policies', 'badge_label', { type: Sequelize.STRING(100), allowNull: false, defaultValue: '' });
await queryInterface.addColumn('plan_policies', 'badge_description', { type: Sequelize.TEXT, allowNull: true });
await queryInterface.addColumn('plan_policies', 'badge_information', { type: Sequelize.TEXT, allowNull: true });
},
};
@@ -0,0 +1,21 @@
'use strict';
// user_tiers.tier was ENUM('free','premium','exclusive').
// Drop the check constraint so it can hold any tier_categories.slug value.
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(
`ALTER TABLE user_tiers DROP CONSTRAINT IF EXISTS check_tier`
);
await queryInterface.sequelize.query(
`ALTER TABLE user_tiers DROP CONSTRAINT IF EXISTS "user_tiers_tier_check"`
);
},
async down(queryInterface) {
await queryInterface.sequelize.query(
`ALTER TABLE user_tiers ADD CONSTRAINT check_tier CHECK (tier IN ('free', 'premium', 'exclusive'))`
);
},
};