All checks were successful
Deploy to production / deploy (push) Successful in 2m51s
- Introduced a new lesson titled "Zahlen & Preise" to enhance the numerical curriculum in Bisaya. - Updated the course creation logic to check for existing "Familien"-Bisaya courses, ensuring idempotency in course creation. - Enhanced the lesson didactics by mapping legacy titles to current lesson keys, improving data consistency. - Adjusted the course generation script to limit Bisaya courses to German-speaking learners only, streamlining course offerings.
140 lines
4.7 KiB
JavaScript
140 lines
4.7 KiB
JavaScript
#!/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);
|
||
});
|