mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
added new requirements for lessons and units
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -1,79 +0,0 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- JUNCTION REVAMP — Units and Lessons run independently (Jul. 7, 2026)
|
||||
--
|
||||
-- courses ⇄ course_units ⇄ units ⇄ unit_lessons ⇄ lessons
|
||||
--
|
||||
-- Run order matters: create → backfill → relax progress FKs → drop old columns.
|
||||
-- Postgres / CockroachDB compatible. Take a backup before running.
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ── 1. Junction tables ───────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE course_units (
|
||||
course_unit_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
course_id BIGINT NOT NULL REFERENCES courses (course_id) ON DELETE CASCADE,
|
||||
unit_id BIGINT NOT NULL REFERENCES units (unit_id) ON DELETE CASCADE,
|
||||
order_index INTEGER NOT NULL DEFAULT 0,
|
||||
"createdBy" BIGINT NULL,
|
||||
"updatedBy" BIGINT NULL,
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uq_course_units_course_unit UNIQUE (course_id, unit_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_course_units_course_id ON course_units (course_id);
|
||||
CREATE INDEX idx_course_units_unit_id ON course_units (unit_id);
|
||||
CREATE INDEX idx_course_units_order ON course_units (order_index);
|
||||
|
||||
CREATE TABLE unit_lessons (
|
||||
unit_lesson_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
unit_id BIGINT NOT NULL REFERENCES units (unit_id) ON DELETE CASCADE,
|
||||
lesson_id BIGINT NOT NULL REFERENCES lessons (lesson_id) ON DELETE CASCADE,
|
||||
order_index INTEGER NOT NULL DEFAULT 0,
|
||||
"createdBy" BIGINT NULL,
|
||||
"updatedBy" BIGINT NULL,
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uq_unit_lessons_unit_lesson UNIQUE (unit_id, lesson_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_unit_lessons_unit_id ON unit_lessons (unit_id);
|
||||
CREATE INDEX idx_unit_lessons_lesson_id ON unit_lessons (lesson_id);
|
||||
CREATE INDEX idx_unit_lessons_order ON unit_lessons (order_index);
|
||||
|
||||
-- ── 2. Backfill from the old direct FKs (preserves ordering) ─────────────────
|
||||
|
||||
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;
|
||||
|
||||
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 — standalone reads carry NULL course/unit context ─────
|
||||
|
||||
ALTER TABLE unit_reading_progress ALTER COLUMN course_id DROP NOT NULL;
|
||||
ALTER TABLE lesson_reading_progress ALTER COLUMN course_id DROP NOT NULL;
|
||||
ALTER TABLE lesson_reading_progress ALTER COLUMN unit_id DROP NOT NULL;
|
||||
|
||||
-- ── 4. Drop the old FK + order columns ───────────────────────────────────────
|
||||
-- (dependent indexes/constraints on these columns are dropped automatically)
|
||||
|
||||
ALTER TABLE units DROP COLUMN course_id;
|
||||
ALTER TABLE units DROP COLUMN order_index;
|
||||
ALTER TABLE lessons DROP COLUMN unit_id;
|
||||
ALTER TABLE lessons DROP COLUMN order_index;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- ── Sanity checks (run after commit) ─────────────────────────────────────────
|
||||
-- SELECT COUNT(*) FROM course_units; -- should equal old COUNT(*) FROM units WHERE course_id IS NOT NULL
|
||||
-- SELECT COUNT(*) FROM unit_lessons; -- should equal old COUNT(*) FROM lessons WHERE unit_id IS NOT NULL
|
||||
-- SELECT c.title, u.title, cu.order_index FROM course_units cu
|
||||
-- JOIN courses c USING (course_id) JOIN units u USING (unit_id)
|
||||
-- ORDER BY c.title, cu.order_index LIMIT 20;
|
||||
@@ -0,0 +1,45 @@
|
||||
'use strict';
|
||||
|
||||
// Sequelize's createTable() wraps ENUM creation in a `DO $$...$$` block for
|
||||
// idempotency, which CockroachDB rejects ("CREATE TYPE usage inside a function
|
||||
// definition is not supported"). Built as raw SQL with STRING + CHECK
|
||||
// constraints instead, matching the pattern established in
|
||||
// 20260710000001-add-status-to-courses.js. Sequelize's DataTypes.ENUM at the
|
||||
// model layer works fine against a STRING+CHECK column — no model-side change
|
||||
// needed.
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE completion_requirements (
|
||||
requirement_id UUID PRIMARY KEY,
|
||||
entity_type STRING NOT NULL,
|
||||
entity_id BIGINT NOT NULL,
|
||||
type STRING NOT NULL,
|
||||
min_percent INTEGER NULL,
|
||||
button_label STRING NULL,
|
||||
is_required BOOLEAN NOT NULL DEFAULT true,
|
||||
"order" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdBy" BIGINT NULL,
|
||||
"updatedBy" BIGINT NULL,
|
||||
"deletedBy" BIGINT NULL,
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"deletedAt" TIMESTAMPTZ NULL,
|
||||
CONSTRAINT check_cr_entity_type CHECK (entity_type IN ('course', 'unit', 'lesson')),
|
||||
CONSTRAINT check_cr_type CHECK (type IN ('read_all_content', 'pass_quiz', 'watch_percent', 'manual_complete'))
|
||||
);
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
`CREATE INDEX idx_cr_entity_type_entity_id ON completion_requirements (entity_type, entity_id);`
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
`CREATE INDEX idx_cr_type ON completion_requirements (type);`
|
||||
);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS completion_requirements;`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
// Raw SQL for the same CockroachDB DO-block/CREATE TYPE reason as
|
||||
// 20260714000001-create-completion-requirements.js.
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE completion_requirement_progress (
|
||||
progress_id UUID PRIMARY KEY,
|
||||
requirement_id UUID NOT NULL REFERENCES completion_requirements (requirement_id) ON DELETE CASCADE,
|
||||
user_id BIGINT NOT NULL REFERENCES users (user_id) ON DELETE CASCADE,
|
||||
entity_type STRING NOT NULL,
|
||||
entity_id BIGINT NOT NULL,
|
||||
progress_percent INTEGER NULL,
|
||||
completed BOOLEAN NOT NULL DEFAULT false,
|
||||
completed_at TIMESTAMPTZ NULL,
|
||||
"createdBy" BIGINT NULL,
|
||||
"updatedBy" BIGINT NULL,
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT check_crp2_entity_type CHECK (entity_type IN ('course', 'unit', 'lesson')),
|
||||
CONSTRAINT uq_crp2_requirement_user UNIQUE (requirement_id, user_id)
|
||||
);
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
`CREATE INDEX idx_crp2_user_id ON completion_requirement_progress (user_id);`
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
`CREATE INDEX idx_crp2_entity_type_entity_id ON completion_requirement_progress (entity_type, entity_id);`
|
||||
);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS completion_requirement_progress;`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
// task_requirements.type IS a real DB enum (task_requirement_type), despite
|
||||
// 20260709000001-add-requires-review-and-prompt-to-task-requirements.js's comment
|
||||
// claiming otherwise ("Sequelize enforces the ENUM only at the application layer
|
||||
// for this table, so no constraint migration is needed") — that assumption was
|
||||
// wrong. submit_text and pass_quiz were added to the Sequelize model (task.mdl.js)
|
||||
// and the admin RequirementBuilder.jsx UI at the same time, but never to the actual
|
||||
// DB enum, so creating/querying either type has been failing with
|
||||
// "invalid input value for enum task_requirement_type" ever since (found while
|
||||
// verifying the new completion_requirements feature's task auto-complete path,
|
||||
// which happened to be the first thing to actually query a pass_quiz-typed
|
||||
// TaskRequirement against this DB).
|
||||
//
|
||||
// ALTER TYPE ... ADD VALUE (not CREATE TYPE) is a plain top-level statement, not
|
||||
// wrapped in Sequelize's problematic DO $$...$$ block, so this runs fine as a
|
||||
// normal migration — no raw-SQL-outside-migrations workaround needed here.
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TYPE task_requirement_type ADD VALUE IF NOT EXISTS 'submit_text'`
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TYPE task_requirement_type ADD VALUE IF NOT EXISTS 'pass_quiz'`
|
||||
);
|
||||
},
|
||||
|
||||
// Postgres/CockroachDB cannot remove a value from an enum type — down is a no-op.
|
||||
async down() {},
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
'use strict';
|
||||
|
||||
// Adds two new completion-requirement types — watch_video and listen_audio —
|
||||
// alongside the existing watch_percent (adjustable-percent) type. Unlike
|
||||
// watch_percent, which tracks one aggregate percent across whichever
|
||||
// video/audio block the learner happens to be playing, these two require
|
||||
// EVERY block of the matching type on the lesson to individually reach 100%,
|
||||
// so per-block progress needs its own column (block_progress) rather than
|
||||
// reusing the single progress_percent figure.
|
||||
//
|
||||
// Same CockroachDB constraint as 20260714000001 — DROP+ADD CONSTRAINT instead
|
||||
// of an ENUM ALTER, since CockroachDB rejects CREATE TYPE inside the DO $$...$$
|
||||
// block Sequelize wraps enum changes in.
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE completion_requirements DROP CONSTRAINT check_cr_type;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE completion_requirements
|
||||
ADD CONSTRAINT check_cr_type
|
||||
CHECK (type IN ('read_all_content', 'pass_quiz', 'watch_percent', 'manual_complete', 'watch_video', 'listen_audio'));
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE completion_requirement_progress
|
||||
ADD COLUMN block_progress JSONB NULL;
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE completion_requirement_progress DROP COLUMN IF EXISTS block_progress;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE completion_requirements DROP CONSTRAINT check_cr_type;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE completion_requirements
|
||||
ADD CONSTRAINT check_cr_type
|
||||
CHECK (type IN ('read_all_content', 'pass_quiz', 'watch_percent', 'manual_complete'));
|
||||
`);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user