mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
course,tasklist,task and completed validation
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.createTable('task_prerequisites', {
|
||||
id: { type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, primaryKey: true },
|
||||
task_id: { type: Sequelize.UUID, allowNull: false, references: { model: 'tasks', key: 'task_id' }, onDelete: 'CASCADE' },
|
||||
prerequisite_task_id: { type: Sequelize.UUID, allowNull: false, references: { model: 'tasks', key: 'task_id' }, onDelete: 'CASCADE' },
|
||||
createdAt: { type: Sequelize.DATE, allowNull: false },
|
||||
updatedAt: { type: Sequelize.DATE, allowNull: false },
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('task_prerequisites', { fields: ['task_id', 'prerequisite_task_id'], unique: true, name: 'uq_task_prerequisite' });
|
||||
await queryInterface.addIndex('task_prerequisites', { fields: ['task_id'], name: 'idx_tp_task_id' });
|
||||
await queryInterface.addIndex('task_prerequisites', { fields: ['prerequisite_task_id'], name: 'idx_tp_prerequisite_task_id' });
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.dropTable('task_prerequisites');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
'use strict';
|
||||
|
||||
// Reshapes completion_requirement_progress.block_progress from
|
||||
// { [blockId]: percentNumber } to { [blockId]: { percent, updatedAt } } so
|
||||
// recordWatchProgress can validate a reported percent against real elapsed
|
||||
// wall-clock time since that block's last sample (anti-skip hardening —
|
||||
// closes the "one API call reports 100%" gaming hole). Applies uniformly to
|
||||
// watch_percent rows too, which start using block_progress as of this change
|
||||
// (previously only watch_video/listen_audio populated it).
|
||||
//
|
||||
// Data-only migration — the column type itself (JSONB) doesn't change, so
|
||||
// this is a plain JS loop + parameterized UPDATE rather than raw jsonb SQL,
|
||||
// consistent with this table's CockroachDB-compatibility precedent (see
|
||||
// 20260715000002, which avoids native ALTER TYPE/enum SQL for the same reason).
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
const rows = await queryInterface.sequelize.query(
|
||||
`SELECT progress_id, block_progress, "updatedAt" FROM completion_requirement_progress WHERE block_progress IS NOT NULL`,
|
||||
{ type: queryInterface.sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
for (const row of rows) {
|
||||
const bp = row.block_progress ?? {};
|
||||
const alreadyShaped = Object.values(bp).every((v) => v && typeof v === 'object');
|
||||
if (alreadyShaped) continue;
|
||||
|
||||
const reshaped = Object.fromEntries(
|
||||
Object.entries(bp).map(([blockId, percent]) => [
|
||||
blockId, { percent: Number(percent) || 0, updatedAt: row.updatedAt ?? new Date().toISOString() },
|
||||
])
|
||||
);
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
`UPDATE completion_requirement_progress SET block_progress = :bp WHERE progress_id = :id`,
|
||||
{ replacements: { bp: JSON.stringify(reshaped), id: row.progress_id } }
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
const rows = await queryInterface.sequelize.query(
|
||||
`SELECT progress_id, block_progress FROM completion_requirement_progress WHERE block_progress IS NOT NULL`,
|
||||
{ type: queryInterface.sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
for (const row of rows) {
|
||||
const bp = row.block_progress ?? {};
|
||||
const flattened = Object.fromEntries(
|
||||
Object.entries(bp).map(([blockId, v]) => [blockId, v?.percent ?? v])
|
||||
);
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
`UPDATE completion_requirement_progress SET block_progress = :bp WHERE progress_id = :id`,
|
||||
{ replacements: { bp: JSON.stringify(flattened), id: row.progress_id } }
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
// Raw SQL for the same CockroachDB DO-block/CREATE TYPE reason as
|
||||
// 20260714000002-create-completion-requirement-progress.js.
|
||||
//
|
||||
// Decoupled from completion_requirements entirely — tracks last-known playback
|
||||
// position for ANY video/audio block, regardless of whether the lesson has a
|
||||
// watch-type completion requirement configured. Powers resume-on-reopen only.
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE media_playback_positions (
|
||||
position_id UUID PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users (user_id) ON DELETE CASCADE,
|
||||
lesson_id BIGINT NOT NULL REFERENCES lessons (lesson_id) ON DELETE CASCADE,
|
||||
block_id STRING NOT NULL,
|
||||
percent INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uq_mpp_user_lesson_block UNIQUE (user_id, lesson_id, block_id)
|
||||
);
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
`CREATE INDEX idx_mpp_user_lesson ON media_playback_positions (user_id, lesson_id);`
|
||||
);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS media_playback_positions;`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.createTable('course_roles', {
|
||||
role_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.STRING(255), 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_roles', ['course_id']);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.dropTable('course_roles');
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user