Files
yourpart3/backend/scripts/dedupe-bisaya-family-courses.js
Torsten Schulz (local) 1ab9407e79
All checks were successful
Deploy to production / deploy (push) Successful in 2m56s
Refactor code structure for improved readability and maintainability; optimize performance across multiple modules.
2026-07-21 09:08:15 +02:00

140 lines
4.7 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Entfernt doppelte „Bisaya für Familien - Alltag & Stabilisierung“-Kurse und behält
* den Kurs mit der kleinsten ID (üblicherweise der erste / in der UI verlinkte).
*
* Standard: Nur Anzeige (Dry-Run). Löschen nur mit:
* VOCAB_DEDUPE_BISAYA_FAMILY_APPLY=1 node backend/scripts/dedupe-bisaya-family-courses.js
*/
import { sequelize } from '../utils/sequelize.js';
const CANONICAL_TITLE = 'Bisaya für Familien - Alltag & Stabilisierung';
async function deleteCourseCascade(courseId, transaction) {
const lessonRows = await sequelize.query(
`SELECT id FROM community.vocab_course_lesson WHERE course_id = :cid`,
{ replacements: { cid: courseId }, transaction, type: sequelize.QueryTypes.SELECT }
);
const lessonIds = lessonRows.map((r) => r.id);
if (lessonIds.length > 0) {
const exRows = await sequelize.query(
`SELECT id FROM community.vocab_grammar_exercise WHERE lesson_id IN (:lids)`,
{ replacements: { lids: lessonIds }, transaction, type: sequelize.QueryTypes.SELECT }
);
const exerciseIds = exRows.map((r) => r.id);
if (exerciseIds.length > 0) {
await sequelize.query(
`DELETE FROM community.vocab_grammar_exercise_progress WHERE exercise_id IN (:eids)`,
{ replacements: { eids: exerciseIds }, transaction }
);
await sequelize.query(`DELETE FROM community.vocab_grammar_exercise WHERE id IN (:eids)`, {
replacements: { eids: exerciseIds },
transaction
});
}
await sequelize.query(
`DELETE FROM community.vocab_course_progress WHERE lesson_id IN (:lids) OR course_id = :cid`,
{ replacements: { lids: lessonIds, cid: courseId }, transaction }
);
await sequelize.query(
`DELETE FROM community.vocab_course_lesson WHERE course_id = :cid`,
{ replacements: { cid: courseId }, transaction }
);
} else {
await sequelize.query(
`DELETE FROM community.vocab_course_progress WHERE course_id = :cid`,
{ replacements: { cid: courseId }, transaction }
);
}
await sequelize.query(
`DELETE FROM community.vocab_course_enrollment WHERE course_id = :cid`,
{ replacements: { cid: courseId }, transaction }
);
await sequelize.query(`DELETE FROM community.vocab_course WHERE id = :cid`, {
replacements: { cid: courseId },
transaction
});
}
async function main() {
const apply = process.env.VOCAB_DEDUPE_BISAYA_FAMILY_APPLY === '1';
await sequelize.authenticate();
const [bisayaLanguage] = await sequelize.query(
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
{ type: sequelize.QueryTypes.SELECT }
);
if (!bisayaLanguage) {
console.error('❌ Bisaya-Sprache nicht gefunden.');
process.exit(1);
}
const courses = await sequelize.query(
`SELECT id, title, owner_user_id AS "ownerUserId", created_at AS "createdAt"
FROM community.vocab_course
WHERE language_id = :languageId AND title = :title
ORDER BY id ASC`,
{
replacements: { languageId: bisayaLanguage.id, title: CANONICAL_TITLE },
type: sequelize.QueryTypes.SELECT
}
);
if (courses.length <= 1) {
console.log(
courses.length === 0
? `Kein Kurs mit Titel „${CANONICAL_TITLE}“ gefunden.`
: `Nur ein Kurs nichts zu bereinigen (ID ${courses[0].id}).`
);
await sequelize.close();
return;
}
const keep = courses[0];
const remove = courses.slice(1);
console.log(`Behalten: Kurs-ID ${keep.id} (älteste ID)\n`);
console.log(`${apply ? 'Löschen' : 'Würde löschen'} (${remove.length} Duplikate):`);
for (const c of remove) {
const stat = await sequelize.query(
`SELECT
(SELECT COUNT(*)::int FROM community.vocab_course_enrollment WHERE course_id = :cid) AS enroll,
(SELECT COUNT(*)::int FROM community.vocab_course_progress WHERE course_id = :cid) AS prog`,
{ replacements: { cid: c.id }, type: sequelize.QueryTypes.SELECT }
);
const row = stat[0];
console.log(
` ID ${c.id} (owner ${c.ownerUserId}, Enrollments ${row?.enroll ?? '?'}, Fortschrittszeilen ${row?.prog ?? '?'})`
);
}
if (!apply) {
console.log(
'\nDry-Run. Zum tatsächlichen Löschen: VOCAB_DEDUPE_BISAYA_FAMILY_APPLY=1 node backend/scripts/dedupe-bisaya-family-courses.js'
);
await sequelize.close();
return;
}
await sequelize.transaction(async (t) => {
for (const c of remove) {
await deleteCourseCascade(c.id, t);
console.log(`🗑️ Kurs ${c.id} entfernt.`);
}
});
console.log(`\n✅ Fertig. Verbleibender Kurs: ID ${keep.id}`);
await sequelize.close();
}
main().catch((e) => {
console.error('❌ Fehler:', e);
sequelize.close();
process.exit(1);
});