Compare commits

...

3 Commits

Author SHA1 Message Date
ebdd75e03f fix: invalidate caches after deleting QSOs
After deleting all QSOs, invalidate the stats and user caches so the
QSO page shows updated statistics instead of stale cached data.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 09:28:37 +01:00
205b311244 fix: handle foreign key constraints when deleting QSOs
The qso_changes table has a foreign key reference to qsos.id, which
was preventing QSO deletion. Now deletes related qso_changes records
first before deleting QSOs.

Also added better error logging to the DELETE endpoint.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 09:26:43 +01:00
6bc0a2f9b2 fix: return correct count from deleteQSOs function
The db.delete() returns a result object with a 'changes' property
indicating the number of affected rows, not the count directly.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 09:22:13 +01:00
2 changed files with 51 additions and 2 deletions

View File

@@ -867,6 +867,7 @@ const app = new Elysia()
message: `Deleted ${deleted} QSO(s)`,
};
} catch (error) {
logger.error('Failed to delete QSOs', { error: error.message, stack: error.stack });
set.status = 500;
return {
success: false,

View File

@@ -1,5 +1,5 @@
import { db, logger } from '../config.js';
import { qsos, qsoChanges } from '../db/schema/index.js';
import { qsos, qsoChanges, syncJobs, awardProgress } from '../db/schema/index.js';
import { max, sql, eq, and, or, desc, like } from 'drizzle-orm';
import { updateJobProgress } from './job-queue.service.js';
import { parseADIF, normalizeBand, normalizeMode } from '../utils/adif-parser.js';
@@ -609,10 +609,58 @@ export async function getLastLoTWQSLDate(userId) {
/**
* Delete all QSOs for a user
* Also deletes related qso_changes records to satisfy foreign key constraints
*/
export async function deleteQSOs(userId) {
logger.debug('Deleting all QSOs for user', { userId });
// Step 1: Delete qso_changes that reference QSOs for this user
// Need to use a subquery since qso_changes doesn't have userId directly
const qsoIdsResult = await db
.select({ id: qsos.id })
.from(qsos)
.where(eq(qsos.userId, userId));
const qsoIds = qsoIdsResult.map(r => r.id);
let deletedChanges = 0;
if (qsoIds.length > 0) {
// Delete qso_changes where qsoId is in the list of QSO IDs
const changesResult = await db
.delete(qsoChanges)
.where(sql`${qsoChanges.qsoId} IN ${sql.raw(`(${qsoIds.join(',')})`)}`);
deletedChanges = changesResult.changes || changesResult || 0;
logger.debug('Deleted qso_changes', { count: deletedChanges });
}
// Step 2: Delete the QSOs
const result = await db.delete(qsos).where(eq(qsos.userId, userId));
return result;
logger.debug('Delete result', { result, type: typeof result, keys: Object.keys(result || {}) });
// Drizzle with SQLite/bun:sqlite returns various formats depending on driver
let count = 0;
if (result) {
if (typeof result === 'number') {
count = result;
} else if (result.changes !== undefined) {
count = result.changes;
} else if (result.rows !== undefined) {
count = result.rows;
} else if (result.meta?.changes !== undefined) {
count = result.meta.changes;
} else if (result.meta?.rows !== undefined) {
count = result.meta.rows;
}
}
logger.info('Deleted QSOs', { userId, count, deletedChanges });
// Invalidate caches for this user
await invalidateStatsCache(userId);
await invalidateUserCache(userId);
return count;
}
/**