added new requirements for lessons and units

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-15 16:26:09 +08:00
parent b7d62b3b18
commit 9c82b0de09
25 changed files with 1569 additions and 233 deletions
@@ -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'));
`);
},
};