'use strict'; /** * Junction revamp — Units and Lessons run independently. * * courses ⇄ course_units ⇄ units ⇄ unit_lessons ⇄ lessons * * 1. Create course_units + unit_lessons (ordering lives on the junction rows) * 2. Backfill from the old direct FKs (units.course_id / lessons.unit_id), * preserving each row's order_index * 3. Progress tables accept NULL course/unit context (standalone reads) * 4. Drop the old FK + order columns from units / lessons */ module.exports = { async up(queryInterface, Sequelize) { // ── 1. Junction tables ──────────────────────────────────────────────────── await queryInterface.createTable('course_units', { course_unit_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true }, 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' }, order_index: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 }, createdBy: { type: Sequelize.BIGINT, allowNull: true }, updatedBy: { type: Sequelize.BIGINT, allowNull: true }, createdAt: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') }, updatedAt: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') }, }); await queryInterface.addIndex('course_units', { fields: ['course_id', 'unit_id'], unique: true, name: 'uq_course_units_course_unit' }); await queryInterface.addIndex('course_units', { fields: ['course_id'], name: 'idx_course_units_course_id' }); await queryInterface.addIndex('course_units', { fields: ['unit_id'], name: 'idx_course_units_unit_id' }); await queryInterface.addIndex('course_units', { fields: ['order_index'], name: 'idx_course_units_order' }); await queryInterface.createTable('unit_lessons', { unit_lesson_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true }, 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' }, order_index: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 }, createdBy: { type: Sequelize.BIGINT, allowNull: true }, updatedBy: { type: Sequelize.BIGINT, allowNull: true }, createdAt: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') }, updatedAt: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') }, }); await queryInterface.addIndex('unit_lessons', { fields: ['unit_id', 'lesson_id'], unique: true, name: 'uq_unit_lessons_unit_lesson' }); await queryInterface.addIndex('unit_lessons', { fields: ['unit_id'], name: 'idx_unit_lessons_unit_id' }); await queryInterface.addIndex('unit_lessons', { fields: ['lesson_id'], name: 'idx_unit_lessons_lesson_id' }); await queryInterface.addIndex('unit_lessons', { fields: ['order_index'], name: 'idx_unit_lessons_order' }); // ── 2. Backfill from the old direct FKs ─────────────────────────────────── await queryInterface.sequelize.query(` INSERT INTO course_units (course_id, unit_id, order_index, "createdBy", "createdAt", "updatedAt") SELECT u.course_id, u.unit_id, COALESCE(u.order_index, 0), u."createdBy", CURRENT_TIMESTAMP, CURRENT_TIMESTAMP FROM units u WHERE u.course_id IS NOT NULL `); await queryInterface.sequelize.query(` INSERT INTO unit_lessons (unit_id, lesson_id, order_index, "createdBy", "createdAt", "updatedAt") SELECT l.unit_id, l.lesson_id, COALESCE(l.order_index, 0), l."createdBy", CURRENT_TIMESTAMP, CURRENT_TIMESTAMP FROM lessons l WHERE l.unit_id IS NOT NULL `); // ── 3. Progress tables — allow standalone (course-less / unit-less) reads ── await queryInterface.changeColumn('unit_reading_progress', 'course_id', { type: Sequelize.BIGINT, allowNull: true }); await queryInterface.changeColumn('lesson_reading_progress', 'course_id', { type: Sequelize.BIGINT, allowNull: true }); await queryInterface.changeColumn('lesson_reading_progress', 'unit_id', { type: Sequelize.BIGINT, allowNull: true }); // ── 4. Drop the old FK + order columns ──────────────────────────────────── await queryInterface.removeColumn('units', 'course_id'); await queryInterface.removeColumn('units', 'order_index'); await queryInterface.removeColumn('lessons', 'unit_id'); await queryInterface.removeColumn('lessons', 'order_index'); }, async down(queryInterface, Sequelize) { // Recreate the direct FK columns and restore one parent per child await queryInterface.addColumn('units', 'course_id', { type: Sequelize.BIGINT, allowNull: true }); await queryInterface.addColumn('units', 'order_index', { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 }); await queryInterface.addColumn('lessons', 'unit_id', { type: Sequelize.BIGINT, allowNull: true }); await queryInterface.addColumn('lessons', 'order_index', { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 }); // Keep the FIRST attachment per child (multi-parent data collapses) await queryInterface.sequelize.query(` UPDATE units u SET course_id = cu.course_id, order_index = cu.order_index FROM (SELECT DISTINCT ON (unit_id) unit_id, course_id, order_index FROM course_units ORDER BY unit_id, course_unit_id) cu WHERE u.unit_id = cu.unit_id `); await queryInterface.sequelize.query(` UPDATE lessons l SET unit_id = ul.unit_id, order_index = ul.order_index FROM (SELECT DISTINCT ON (lesson_id) lesson_id, unit_id, order_index FROM unit_lessons ORDER BY lesson_id, unit_lesson_id) ul WHERE l.lesson_id = ul.lesson_id `); await queryInterface.changeColumn('unit_reading_progress', 'course_id', { type: Sequelize.BIGINT, allowNull: false }); await queryInterface.changeColumn('lesson_reading_progress', 'course_id', { type: Sequelize.BIGINT, allowNull: false }); await queryInterface.changeColumn('lesson_reading_progress', 'unit_id', { type: Sequelize.BIGINT, allowNull: false }); await queryInterface.dropTable('unit_lessons'); await queryInterface.dropTable('course_units'); }, };