AI Implementation feature(903): Admin Area Challenges: List, Import/Export and Add/Edit Modal 1.03 (#44)

This commit was merged in pull request #44.
This commit is contained in:
2026-07-22 22:18:10 +00:00
parent fff0a600df
commit dcda3dfd4d
11 changed files with 583 additions and 112 deletions
@@ -446,24 +446,29 @@ export class AdminChallengesService {
}
const result: ImportResult = { imported: 0, skipped: [], warnings: [] };
let orphanedStoredFilenames: string[] = [];
try {
await this.dataSource.transaction(async (manager) => {
const challengeRepo = manager.getRepository(ChallengeEntity);
const fileRepo = manager.getRepository(ChallengeFileEntity);
// Full-replace: capture every existing challenge and its disk-bound
// files so we can unlink them after the wipe, then delete every row.
// ChallengeFileEntity and SolveEntity both cascade on challenge delete.
const existingChallenges = await challengeRepo.find();
const existingIds = existingChallenges.map((c) => c.id);
if (existingIds.length > 0) {
const existingFiles = await fileRepo.find({ where: { challengeId: In(existingIds) } });
orphanedStoredFilenames = existingFiles.map((f) => f.storedFilename);
await manager.delete(ChallengeEntity, { id: In(existingIds) });
}
for (const ch of doc.challenges) {
const cat = catMap.get(ch.categorySystemKey.toLowerCase());
if (!cat) {
result.skipped.push({ name: ch.name, reason: `Unknown category: ${ch.categorySystemKey}` });
continue;
}
// Upsert by case-insensitive name.
const existing = await challengeRepo
.createQueryBuilder('c')
.where('LOWER(c.name) = :n', { n: ch.name.toLowerCase() })
.getOne();
if (existing) {
await manager.delete(ChallengeEntity, { id: existing.id });
}
const row = challengeRepo.create({
id: uuid(),
name: ch.name,
@@ -501,6 +506,11 @@ export class AdminChallengesService {
throw err;
}
// Best-effort unlink of files owned by wiped challenges (post-commit).
for (const storedFilename of orphanedStoredFilenames) {
this.fileService.removeFinal(storedFilename);
}
// Commit each staged file by renaming to its final filename (the
// ChallengeFile rows already reference those names).
for (const list of stagedByName.values()) {