Files
starr-philproperties/database/migrations/20260717000002-reshape-block-progress-timestamps.js
T
2026-07-17 13:04:27 +08:00

60 lines
2.5 KiB
JavaScript

'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 } }
);
}
},
};